İlk WebGPU uygulamanız

1. Giriş

WebGPU Logosu, stilize edilmiş bir "W" harfi oluşturan birkaç mavi üçgenden oluşur.

WebGPU nedir?

WebGPU, web uygulamalarında GPU'nuzun özelliklerine erişim sağlayan yeni ve modern bir API'dir.

Modern API

WebGPU'dan önce, WebGPU'nun özelliklerinin bir alt kümesini sunan WebGL vardı. Yeni bir zengin web içeriği sınıfını sağladı ve geliştiriciler bu içerikle harika ürünler yarattı. Ancak bu geliştirme, 2007'de yayınlanan ve daha da eski OpenGL API'ye dayanan OpenGL ES 2.0 API'sini temel alıyordu. Bu dönemde GPU'lar önemli ölçüde gelişti ve Direct3D 12, Metal ve Vulkan'la birlikte arayüzlerle arayüz oluşturmak için kullanılan yerel API'ler de gelişti.

WebGPU, bu modern API'lerin sağladığı gelişmeleri web platformuna taşıyor. GPU özelliklerini platformlar arası olarak etkinleştirmeye odaklanırken web'de doğal görünen ve üzerine derlendiği bazı yerel API'lerden daha az ayrıntılı bir API sunar.

Görüntü Oluşturma

GPU'lar genellikle hızlı ve ayrıntılı grafikler oluşturmakla ilişkilendirilir. WebGPU için de durum farklı değildir. Hem masaüstü hem de mobil GPU'larda günümüzün en popüler oluşturma tekniklerinin birçoğunu desteklemek için gereken özelliklere sahiptir. Ayrıca donanım özellikleri gelişmeye devam ettikçe gelecekte eklenecek yeni özellikler için bir yol sunar.

Bilgi işlem

WebGPU, oluşturmaya ek olarak GPU'nuzun genel amaçlı, son derece paralel iş yükleri gerçekleştirme için potansiyelini ortaya çıkarır. Bu bilgi işlem gölgelendiricileri, herhangi bir oluşturma bileşeni olmadan bağımsız olarak veya oluşturma ardışık düzeninizin sıkı bir şekilde entegre bir parçası olarak kullanılabilir.

Bugünkü codelab'de, basit bir giriş projesi oluşturmak için WebGPU'nun oluşturma ve bilgi işlem özelliklerinden nasıl yararlanacağınızı öğreneceksiniz.

Oluşturacaklarınız

Bu codelab'de WebGPU'yu kullanarak Conway'in Game of Life oyununu geliştireceksiniz. Uygulamanız şunları yapabilecek:

  • Basit 2D grafikler çizmek için WebGPU'nun oluşturma özelliklerini kullanın.
  • Simülasyonu gerçekleştirmek için WebGPU'nun işlem özelliklerini kullanın.

Bu codelab'deki son ürünün ekran görüntüsü

Hücresel otomat olarak bilinen Hayat Oyunu kavramı, hücrelerden oluşan bir ızgaranın zaman içinde bazı kurallara bağlı olarak durumun değiştiği bir yöntemdir. Yaşam Oyunu'nda hücreler, komşu hücrelerden kaç tanesinin aktif olduğuna bağlı olarak etkin ya da devre dışı kalır. Bu durum, siz izlerken ilginç kalıplar oluşmasına neden olur.

Neler öğreneceksiniz?

  • WebGPU kurulumu ve tuval yapılandırma.
  • Basit 2D geometri çizme.
  • Çizilen öğeyi değiştirmek için tepe noktası ve parça gölgelendiricilerin nasıl kullanılacağı.
  • Basit bir simülasyon gerçekleştirmek için hesaplama gölgelendiricileri nasıl kullanılır?

Bu codelab'de, WebGPU'nun arkasındaki temel kavramları tanıtacağız. API'nin kapsamlı bir incelemesi amaçlanmamıştır ve 3D matris matematiği gibi sık sık alakalı konuları kapsamaz (ya da gerektirmemiştir).

Gerekenler

  • ChromeOS, macOS veya Windows'da Chrome'un son sürümü (113 veya üzeri). WebGPU, tarayıcılar arası ve platformlar arası bir API'dır ancak henüz her yerde kullanıma sunulmamıştır.
  • HTML, JavaScript ve Chrome Geliştirici Araçları hakkında bilgi sahibi olmak.

WebGL, Metal, Vulkan veya Direct3D gibi diğer Grafik API'lerini bilmeniz gerekmez. Ancak, bu API'lerle deneyiminiz olursa, hemen öğrenmeye başlamanıza yardımcı olabilecek WebGPU'daki pek çok benzerlik olduğunu görebilirsiniz.

2. Hazırlanın

Kodu alma

Bu codelab'de herhangi bir bağımlılık yoktur. Ayrıca, WebGPU uygulamasını oluşturmak için gereken her adımda size yol gösterir. Bu nedenle, başlamak için herhangi bir koda ihtiyacınız yoktur. Bununla birlikte, kontrol noktası olarak kullanabileceğiniz bazı çalışan örnekleri https://glitch.com/edit/#!/your-first-webgpu-app adresinde bulabilirsiniz. Bu belgelere göz atabilir ve takıldığınız noktalarda referans alabilirsiniz.

Geliştirici Konsolu'nu kullanın!

WebGPU, doğru kullanımı zorunlu kılan birçok kuralın bulunduğu oldukça karmaşık bir API'dir. Daha da kötüsü, API'nin çalışma şekli nedeniyle birçok hata için tipik JavaScript istisnaları bildiremez ve bu da sorunun tam olarak nereden geldiğini tam olarak belirlemeyi zorlaştırır.

Özellikle yeni başlayan bir kullanıcı olarak WebGPU ile geliştirme yaparken sorunlarla karşılaşacaksınız ve bu normal bir durumdur! API'nin arkasındaki geliştiriciler, GPU geliştirmeyle çalışmanın zorluklarının farkındadır ve WebGPU kodunuzun hataya neden olduğu her seferinde geliştirici konsolunda sorunu tanımlayıp düzeltmenize yardımcı olacak çok ayrıntılı ve yararlı mesajlar almanızı sağlamak için çok çaba göstermektedirler.

Herhangi bir web uygulaması üzerinde çalışırken konsolu açık tutmak her zaman yararlıdır, ancak bu özellikle burada da geçerlidir!

3. WebGPU'yu başlat

<canvas> ile başlayın

WebGPU, ekranda hiçbir şey gösterilmeden de kullanılabilir. Bunun için yalnızca hesaplamalar yapmak istiyorsanız WebGPU'yu kullanmanız gerekir. Ancak codelab'de yapacağımız gibi öğeler oluşturmak istiyorsanız bir zemine ihtiyacınız vardır. İyi bir başlangıç yapabilirsiniz.

Tek bir <canvas> öğesi ve tuval öğesini sorguladığımız bir <script> etiketi içeren yeni bir HTML dokümanı oluşturun. (Veya glitch'ten 00-starter-page.html kullanın.)

  • Aşağıdaki kodla bir index.html dosyası oluşturun:

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>

Adaptör ve cihaz isteme

Artık WebGPU bitlerine girebilirsiniz! Öncelikle, WebGPU gibi API'lerin tüm web ekosistemine yayılmasının zaman alabileceğini göz önünde bulundurmalısınız. Sonuç olarak, ilk önlem olarak kullanıcının tarayıcısının WebGPU'yu kullanıp kullanamayacağını kontrol etmek iyi bir uygulamadır.

  1. WebGPU'nun giriş noktası olarak işlev gören navigator.gpu nesnesinin mevcut olup olmadığını kontrol etmek için aşağıdaki kodu ekleyin:

index.html

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

İdeal olarak, sayfayı WebGPU kullanmayan bir moda geri döndürerek WebGPU'nun kullanılamıyorsa WebGPU'yu kullanıcıya bildirmeniz gerekir. (Bunun yerine WebGL'yi kullanabilir miyiz?) Bununla birlikte, bu codelab'in amaçları doğrultusunda, kodun daha fazla yürütülmesini önlemek için bir hata alırsınız.

Tarayıcının WebGPU'yu desteklediğini öğrendikten sonra, uygulamanız için WebGPU'yu başlatmanın ilk adımı bir GPUAdapter isteğinde bulunmaktır. Adaptörü, WebGPU'nun cihazınızdaki belirli bir GPU donanımı parçasını temsili olarak düşünebilirsiniz.

  1. Adaptör almak için navigator.gpu.requestAdapter() yöntemini kullanın. Bir söz döndürüyor. Bu nedenle en uygun seçenek, await ile çağrılmasıdır.

index.html

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

Uygun bağdaştırıcı bulunamazsa döndürülen adapter değeri null olabilir. Bu nedenle bu olasılığı değerlendirmeniz gerekir. Bu durum, kullanıcının tarayıcısı WebGPU'yu destekliyorsa ancak GPU donanımı WebGPU'yu kullanmak için gereken tüm özelliklere sahip değilse oluşabilir.

Çoğu zaman, burada yaptığınız gibi tarayıcının varsayılan bir bağdaştırıcıyı seçmesine izin vermek çoğu zaman sorun yaratmaz, ancak daha ileri düzey ihtiyaçlar için birden fazla GPU'ya sahip cihazlarda (ör. bazı dizüstü bilgisayarlar) düşük güçlü veya yüksek performanslı donanım kullanmak istediğinizi belirten bağımsız değişkenler requestAdapter() ürününe iletilebilir.

Bağdaştırıcınız olduğunda, GPU ile çalışmaya başlamadan önce yapılacak son adım bir GPUDevice isteğinde bulunmaktır. Cihaz, GPU ile en fazla etkileşimin gerçekleştiği ana arayüzdür.

  1. adapter.requestDevice() numaralı telefonu arayarak cihazı alın; bu da size bir söz vermiş olur.

index.html

const device = await adapter.requestDevice();

requestAdapter() ürününde olduğu gibi, burada da belirli donanım özelliklerini etkinleştirmek veya daha yüksek sınırlar istemek gibi daha gelişmiş kullanımlar için seçenekler geçirilebilir.

Canvas'ı yapılandırma

Artık bir cihazınız olduğuna göre, bu cihazı sayfada herhangi bir şeyi göstermek için kullanmak istiyorsanız yapmanız gereken bir işlem daha vardır: Tuvali, yeni oluşturduğunuz cihazla kullanılacak şekilde yapılandırın.

  • Bunu yapmak için önce canvas.getContext("webgpu") numaralı telefonu arayarak kanvastan bir GPUCanvasContext isteyin. (Bu, sırasıyla 2d ve webgl içerik türlerini kullanarak Tuval 2D veya WebGL bağlamlarını başlatmak için kullanacağınız çağrıyla aynıdır.) Sonrasında döndürdüğü context, aşağıdaki gibi configure() yöntemi kullanılarak cihazla ilişkilendirilmelidir:

index.html

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

Burada iletilebilecek birkaç seçenek vardır ancak en önemli olanlar, bağlamı kullanacağınız device ve bağlamın kullanması gereken doku biçimi olan format'tır.

Dokular, WebGPU'nun resim verilerini depolamak için kullandığı nesnelerdir ve her doku, GPU'nun bu verilerin bellekte nasıl yer aldığını bilmesini sağlayan bir biçime sahiptir. Doku belleğinin çalışma şekliyle ilgili ayrıntılar bu codelab'de yer almamaktadır. Bilinmesi gereken önemli nokta, tuval bağlamının kodunuzun içine çizileceği dokular sağladığıdır ve kullandığınız biçimin, tuvalin bu resimleri ne kadar verimli gösterdiği konusunda etkisi olabilir. Farklı doku biçimleri kullanıldığında farklı cihaz türleri en iyi performansı gösterir. Cihazın tercih edilen biçimini kullanmazsanız resim sayfanın bir parçası olarak görüntülenmeden önce arka planda fazladan bellek kopyaları oluşabilir.

WebGPU, tuvaliniz için hangi biçimin kullanılacağını belirttiğinden bunlarla ilgili çok fazla endişelenmenize gerek yok. Neredeyse her durumda, yukarıda gösterildiği gibi navigator.gpu.getPreferredCanvasFormat() çağrısıyla döndürülen değeri iletmek istersiniz.

Kanvası temizleme

Artık bir cihazınız olduğuna ve kanvasın onunla yapılandırıldığına göre tuvalin içeriğini değiştirmek için cihazı kullanmaya başlayabilirsiniz. Başlamak için tek renkle temizleyin.

Bunu veya WebGPU'daki diğer herhangi bir şeyi yapmak için GPU'ya ne yapması gerektiğini belirten bazı komutlar sağlamanız gerekir.

  1. Bunu yapmak için cihazdan, GPU komutlarını kaydetmeye yönelik arayüz sağlayan bir GPUCommandEncoder oluşturmasını isteyin.

index.html

const encoder = device.createCommandEncoder();

GPU'ya göndermek istediğiniz komutlar, oluşturma ile ilgilidir (bu örnekte tuvali temizleme). Bir sonraki adım, Oluşturma Geçişi başlatmak için encoder kullanmaktır.

Oluşturma biletleri, WebGPU'daki tüm çizim işlemleri gerçekleştiğinde gerçekleştirilir. Her biri, gerçekleştirilen çizim komutlarının çıkışını alan dokuları tanımlayan bir beginRenderPass() çağrısıyla başlar. Daha gelişmiş kullanımlar, oluşturulan geometrinin derinliğini depolamak veya kenar yumuşatma sağlamak gibi çeşitli amaçlarla ek adı verilen çeşitli dokular sağlayabilir. Ancak bu uygulama için yalnızca bir tanesine ihtiyacınız vardır.

  1. context.getCurrentTexture() yöntemini çağırarak daha önce oluşturduğunuz kanvas bağlamındaki dokuyu alabilirsiniz. Bu çağrı, kanvasın width ve height özellikleriyle ve context.configure() çağırdığınızda belirttiğiniz format ile eşleşen piksel genişliği ve yüksekliğine sahip bir doku döndürür.

index.html

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

Doku, colorAttachment öğesinin view özelliği olarak verilir. Oluşturma geçişlerinde, dokunun hangi kısımlarında oluşturulduğunu belirten GPUTexture yerine bir GPUTextureView sağlamanız gerekir. Bu yalnızca daha gelişmiş kullanım alanları için gerçekten önemlidir. Dolayısıyla burada, doku üzerinde bağımsız değişken olmadan createView() öğesini çağırırsınız. Bu, oluşturma geçişinin tüm dokuyu kullanmasını istediğinizi belirtir.

Ayrıca, oluşturma geçişinin başladığında ve sona erdiğinde dokuyla ne yapmasını istediğinizi de belirtmeniz gerekir:

  • loadOp değeri "clear", oluşturma geçişi başladığında dokunun temizlenmesini istediğinizi belirtir.
  • storeOp değeri "store", oluşturma geçişi tamamlandıktan sonra, oluşturma geçişi sırasında yapılan tüm çizimlerin sonuçlarının dokuya kaydedilmesini istediğinizi belirtir.

Oluşturma geçişi başladıktan sonra herhangi bir şey yapmanız gerekmez! En azından şimdilik. Oluşturma geçişini loadOp: "clear" ile başlatma işlemi, doku görünümünü ve tuvali temizlemek için yeterlidir.

  1. Oluşturma aktarımını, beginRenderPass() öğesinin hemen sonrasına aşağıdaki çağrıyı ekleyerek sonlandırın:

index.html

pass.end();

Yalnızca bu çağrıları yapmanın GPU'nun gerçekte herhangi bir şey yapmasına neden olmayacağını bilmek önemlidir. Bunlar sadece daha sonra GPU'nun yapması için komutları kaydeder.

  1. GPUCommandBuffer oluşturmak için komut kodlayıcıda finish() yöntemini çağırın. Komut arabelleği, kaydedilen komutlar için opak bir tutma yeridir.

index.html

const commandBuffer = encoder.finish();
  1. GPUDevice öğesinin queue öğesini kullanarak komut arabelleğini GPU'ya gönderin. Sıra, tüm GPU komutlarını yerine getirerek yürütmelerinin doğru sıralandığından ve düzgün şekilde senkronize edildiğinden emin olur. Sıranın submit() yöntemi, bir komut arabelleği dizisi alır ancak bu durumda yalnızca bir arabellek vardır.

index.html

device.queue.submit([commandBuffer]);

Komut arabelleği gönderdiğinizde bu arabellek tekrar kullanılamaz, dolayısıyla saklamaya gerek yoktur. Daha fazla komut göndermek istiyorsanız başka bir komut arabelleği oluşturmanız gerekir. Bu nedenle, bu codelab'in örnek sayfalarında olduğu gibi bu iki adımın tek bir adım altında daraltılmış olarak görülmesi oldukça yaygın bir durumdur:

index.html

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

Komutları GPU'ya gönderdikten sonra, JavaScript'in kontrolü tarayıcıya döndürmesine izin verin. Bu noktada, tarayıcı, bağlamın geçerli dokusunu değiştirdiğinizi görür ve tuvali, bu dokuyu resim olarak görüntüleyecek şekilde günceller. Sonrasında tuval içeriğini tekrar güncellemek isterseniz yeni bir komut arabelleği kaydedip göndermeniz ve oluşturma geçişi için yeni bir doku almak üzere context.getCurrentTexture() öğesini tekrar çağırmanız gerekir.

  1. Sayfayı tekrar yükleyin. Kanvasın siyah renkle doldurulduğuna dikkat edin. Tebrikler! Bu, ilk WebGPU uygulamanızı başarıyla oluşturduğunuz anlamına gelir.

WebGPU&#39;nun tuval içeriklerini temizlemek için başarıyla kullanıldığını gösteren siyah tuval.

Renk seçin

Dürüst olmak gerekirse siyah kareler oldukça sıkıcı. Bu yüzden, içeriği biraz kişiselleştirmek için bir sonraki bölüme geçmeden önce birkaç dakikanızı ayırın.

  1. encoder.beginRenderPass() çağrısında, colorAttachment öğesine clearValue içeren yeni bir satır ekleyin. Örneğin:

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, geçişin başında clear işlemi gerçekleştirilirken oluşturma geçişi için hangi rengin kullanılması gerektiğini belirtir. İletilen sözlük dört değer içerir: red için r, yeşil için g, mavi için b ve alfa (şeffaflık) için a. Her değer 0 ile 1 arasında değişebilir ve birlikte o renk kanalının değerini tanımlar. Örneğin:

  • { r: 1, g: 0, b: 0, a: 1 } parlak kırmızı.
  • { r: 1, g: 0, b: 1, a: 1 } parlak mor.
  • { r: 0, g: 0.3, b: 0, a: 1 } koyu yeşil.
  • { r: 0.5, g: 0.5, b: 0.5, a: 1 } orta gri.
  • { r: 0, g: 0, b: 0, a: 0 } varsayılan şeffaf siyah renktir.

Bu codelab'deki örnek kod ve ekran görüntülerinde koyu mavi kullanılır ancak dilediğiniz rengi seçebilirsiniz.

  1. Renginizi seçtikten sonra sayfayı yeniden yükleyin. Seçtiğiniz rengi tuvalde göreceksiniz.

Tuval, varsayılan net rengin nasıl değiştirileceğini göstermek için koyu mavi renge dönüştürüldü.

4. Geometri çizme

Bu bölümün sonunda uygulamanız tuvale basit bir geometri çizer: renkli bir kare. Böyle basit bir çıktı için çok fazla işmiş gibi görüneceği konusunda sizi uyaralım. Bunun nedeni, WebGPU'nun çok fazla geometriyi çok verimli bir şekilde oluşturmak üzere tasarlanmış olmasıdır. Bu verimliliğin bir yan etkisi olarak görece basit işler yapmak alışılmadık derecede zor gelebilir, ancak WebGPU gibi bir API'ye geçiş yapıyorsanız bu beklenen bir şeydir. Biraz daha karmaşık bir şey yapmak istersiniz.

GPU'ların çizimlerini anlama

Daha fazla kod değişikliği yapmadan önce, GPU'ların ekranda gördüğünüz şekilleri nasıl oluşturduğuna dair çok hızlı, basitleştirilmiş ve üst düzey bir genel bakışın faydalı olacağını düşünüyoruz. (GPU oluşturmanın çalışma şekliyle ilgili temel bilgileri zaten biliyorsanız Köşeleri Tanımlama bölümüne geçebilirsiniz.)

Kullanabileceğiniz çok sayıda şekil ve seçenek bulunan Canvas 2D gibi API'lerden farklı olarak GPU'nuz yalnızca birkaç farklı şekille (veya WebGPU'da temel öğelerle) (noktalar, çizgiler ve üçgenler) çalışır. Bu codelab'in amaçları doğrultusunda yalnızca üçgenler kullanacaksınız.

Üçgenlerin öngörülebilir ve verimli bir şekilde kolayca işlenebilmesini sağlayan çok sayıda güzel matematiksel özelliği olduğundan GPU'lar neredeyse tamamen üçgenlerle çalışır. GPU'nun çizebilmesi için GPU ile çizdiğiniz hemen hemen her şeyin üçgenlere bölünmesi ve bu üçgenlerin köşe noktalarıyla tanımlanması gerekir.

Bu noktalar veya köşeler, WebGPU veya benzer API'ler tarafından tanımlanan sepet koordinat sistemindeki bir noktayı tanımlayan X, Y ve (3D içerik için) Z değerleri cinsinden verilir. Koordinat sisteminin yapısı, sayfanızdaki tuvalle ilişkisi açısından en kolay şekilde düşünülebilir. Tuvalinizin genişliği veya yüksekliği ne olursa olsun, sol kenar X ekseninde her zaman -1'de ve sağ kenar X ekseninde her zaman +1'dedir. Benzer şekilde, alt kenar, Y ekseninde her zaman -1 ve Y ekseninde üst kenar her zaman +1'dir. Diğer bir deyişle, (0, 0) her zaman tuvalin merkezi, (-1, -1) her zaman sol alt köşedir ve (1, 1) her zaman sağ üst köşedir. Bu alana Klip Alanı adı verilir.

Normalleştirilmiş Cihaz Koordinatı alanını görselleştiren basit bir grafik.

Bu koordinat sisteminde köşeler başlangıçta nadiren tanımlanır, bu nedenle GPU'lar, köşeleri klip alanına dönüştürmek için gerekli olan matematiksel işlemleri ve köşeleri çizmek için gereken diğer hesaplamaları gerçekleştirmek üzere köşe gölgelendirici adı verilen küçük programlardan yararlanır. Örneğin, gölgelendirici biraz animasyon uygulayabilir veya tepe noktasından bir ışık kaynağına doğru yönü hesaplayabilir. Bu gölgelendiriciler, WebGPU geliştiricisi olarak sizin tarafınızdan yazılmıştır ve GPU'nun çalışma şekli üzerinde olağanüstü bir kontrol sağlar.

Ardından GPU, dönüştürülen bu köşelerden oluşan tüm üçgenleri alır ve bunları çizmek için ekranda hangi piksellerin gerektiğini belirler. Daha sonra, her bir pikselin hangi rengi olması gerektiğini hesaplayan, parça gölgelendirici adlı, yazdığınız başka bir küçük programı çalıştırır. Bu hesaplama, yeşil dönüş kadar basit olabileceği gibi, yüzeyin yakındaki diğer yüzeylerden salınan güneş ışığıyla göreceli açısını hesaplamak kadar karmaşık olabilir. Bu tümüyle sizin kontrolünüzdedir ve bu hem güçlendirici hem de bunaltıcı olabilir.

Bu piksel renklerinin sonuçları daha sonra bir doku halinde toplanır ve bu doku, ekranda gösterilebilir.

Köşeleri tanımlama

Daha önce belirtildiği gibi, Hayat Oyunu simülasyonu hücrelerden oluşan bir ızgara olarak gösterilir. Uygulamanızın, etkin hücreleri etkin olmayan hücrelerden ayırt ederek ızgarayı görselleştirmek için bir yönteme ihtiyacı var. Bu codelab'de kullanılacak yaklaşım, etkin hücrelere renkli kareler çizmek ve etkin olmayan hücreleri boş bırakmaktır.

Bu, GPU'ya karenin dört köşesinin her biri için bir tane olmak üzere dört farklı nokta sağlamanız gerektiği anlamına gelir. Örneğin, tuvalin merkezinde çizilen ve kenarlardan belirli bir şekilde içe doğru çekilen bir karenin köşe koordinatları şu şekildedir:

Bir karenin köşelerinin koordinatlarını gösteren Normalleştirilmiş Cihaz Koordinatı grafiği

Bu koordinatları GPU'ya aktarmak için değerleri bir TypedArray içine yerleştirmeniz gerekir. Henüz aşina değilseniz TypedArrays, bitişik bellek bloklarını ayırmanıza ve serideki her öğeyi belirli bir veri türü olarak yorumlamanıza olanak tanıyan bir JavaScript nesneleri grubudur. Örneğin, Uint8Array hücresinde, dizideki her öğe tek bir imzasız bayttır. TypedArray'ler; WebAssembly, WebAudio ve (tabii ki) WebGPU gibi bellek düzenine duyarlı API'lerle verileri birbirlerine göndermek için mükemmeldir.

Kare örneği için değerler kesirli olduğundan Float32Array uygundur.

  1. Aşağıdaki dizi bildirimini kodunuza yerleştirerek diyagramdaki köşe konumlarının tümünü barındıran bir dizi oluşturun. Üst tarafa yakın, context.configure() görüşmesinin hemen altında yer almak için iyi bir yer.

index.html

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

Boşlukların ve açıklamanın değerler üzerinde herhangi bir etkisinin olmadığını unutmayın; yalnızca size kolaylık sağlamak ve daha okunabilir hale getirmek içindir. Her değer çiftinin bir köşe noktası için X ve Y koordinatlarını oluşturduğunu görmenize yardımcı olur.

Ama bir sorun var. GPU'lar üçgenler şeklinde çalışır. Hatırlıyor musunuz? Bu, köşeleri üçlü gruplar halinde sağlamanız gerektiği anlamına gelir. Dört kişilik bir grubun var. Çözüm, köşelerden ikisini tekrarlayarak karenin ortasından geçen bir kenar paylaşan iki üçgen oluşturmaktır.

Karenin dört köşesinin iki üçgen oluşturmak için nasıl kullanılacağını gösteren diyagram.

Diyagramdan kare oluşturmak için (-0,8, -0,8) ve (0,8, 0,8) köşe noktalarını bir kez mavi üçgen, diğeri kırmızı üçgen için olmak üzere iki kez listelemeniz gerekir. (Kareyi bunun yerine diğer iki köşesiyle bölmeyi de seçebilirsiniz; fark yaratmaz.)

  1. Önceki vertices dizinizi aşağıdaki gibi görünecek şekilde güncelleyin:

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

Şemada daha net anlaşılması için iki üçgen arasındaki ayrım gösterilse de köşe konumları tam olarak aynıdır ve GPU, bunları boşluk bırakmadan oluşturur. Tek, düz bir kare olarak oluşturulur.

Köşe tamponu oluşturma

GPU, bir JavaScript dizisinden alınan verilerle köşe çizemez. GPU'lar genellikle oluşturma için son derece optimize edilmiş kendi belleğe sahiptir. Bu nedenle, GPU'nun çizim sırasında kullanmasını istediğiniz verilerin bu belleğe yerleştirilmesi gerekir.

Köşe verileri de dahil olmak üzere birçok değer için GPU tarafı belleği, GPUBuffer nesneleri üzerinden yönetilir. Arabellek, GPU'nun kolayca erişebileceği ve belirli amaçlar için işaretlenen bir bellek blokudur. Bunu GPU'nun görünür olduğu TypedArray gibi düşünebilirsiniz.

  1. Köşelerinizi tutacak bir arabellek oluşturmak için aşağıdaki çağrıyı, vertices dizinizin tanımından sonra device.createBuffer() öğesine ekleyin.

index.html

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

Dikkat etmeniz gereken ilk şey arabelleğe bir etiket vermenizdir. Oluşturduğunuz her WebGPU nesnesine isteğe bağlı bir etiket verilebilir ve bu etiketi kesinlikle kullanmak istersiniz! Etiket, nesnenin ne olduğunu tanımlamanıza yardımcı olduğu sürece istediğiniz herhangi bir dizedir. Herhangi bir sorunla karşılaşırsanız bu etiketler, WebGPU'nun ürettiği hata mesajlarında kullanılır. Bu etiketler, sorunun ne olduğunu anlamanıza yardımcı olur.

Ardından, arabellek için bayt cinsinden bir boyut girin. 48 baytlık bir arabelleğe ihtiyacınız vardır. Bu tamponu, 32 bitlik bir kayan öğenin ( 4 bayt) boyutunu vertices dizinizdeki (12) kayan noktalı bir sayıyla çarparak belirlersiniz. Neyse ki TypedArrays, byteLength'u sizin için zaten hesaplar ve arabelleği oluştururken bu değeri kullanabilirsiniz.

Son olarak, arabelleğin kullanımını belirtmeniz gerekir. Bu, | ( bit tabanlı VEYA) operatörüyle birleştirilen birden fazla işaretin olduğu GPUBufferUsage işaretlerinden biri veya daha fazlasıdır. Bu durumda, arabelleğin köşe verileri için kullanılmasını (GPUBufferUsage.VERTEX) ve verileri de bu verilere kopyalayabilmek (GPUBufferUsage.COPY_DST) istediğinizi belirtirsiniz.

Size döndürülen arabellek nesnesi opak olur, içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, özelliklerinin çoğu sabittir. Bir GPUBuffer oluşturulduktan sonra yeniden boyutlandırılamaz veya kullanım işaretleri değiştirilemez. Bunları, belleğin içeriğini değiştirebilirsiniz.

Arabellek ilk oluşturulduğunda, içerdiği bellek sıfır olarak başlatılır. İçeriğini değiştirmenin birkaç yolu vardır ancak en kolay yöntem device.queue.writeBuffer() öğesini, kopyalamak istediğiniz TypedArray ile çağırmaktır.

  1. Köşe verilerini arabelleğin belleğine kopyalamak için aşağıdaki kodu ekleyin:

index.html

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

Köşe düzenini tanımlama

Şimdi içinde köşe verileri olan bir tamponunuz var ancak GPU söz konusu olduğunda bu yalnızca bir bayt blob'udur. Bir şeyler çizecekseniz biraz daha bilgi vermeniz gerekir. WebGPU'ya köşe verilerinin yapısı hakkında daha fazla bilgi verebilmeniz gerekir.

index.html

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

Bu, ilk bakışta biraz kafa karıştırıcı olabilir ancak ayrılması nispeten daha kolaydır.

Vereceğiniz ilk şey arrayStride. Bu, GPU'nun bir sonraki köşeyi ararken arabellekte ileri atlaması gereken bayt sayısıdır. Karenizin her tepe noktası, 32 bitlik iki kayan nokta sayısından oluşur. Daha önce belirtildiği gibi, 32 bitlik bir kayan nokta 4 bayttır. Yani iki kayan öğe 8 bayttır.

Sırada, bir dizi olan attributes özelliği var. Nitelikler, her bir tepe noktasına kodlanan bağımsız bilgi parçalarıdır. Köşeler yalnızca bir özellik (köşe konumu) içerir, ancak daha gelişmiş kullanım alanlarında çoğu zaman tepe noktasının rengi veya geometri yüzeyinin işaret ettiği yön gibi birden çok özellik bulunan köşeler bulunur. Ancak bu, bu codelab'in kapsamı dışındadır.

Tek özelliğinizde, önce verilerin format değerini tanımlarsınız. Bu metrik, GPU'nun anlayabileceği her köşe verisi türünü açıklayan GPUVertexFormat türleri listesinden gelir. Köşelerin her birinde 32 bitlik iki kayan nokta olduğu için float32x2 biçimini kullanırsınız. Örneğin, tepe noktası verilerinizin her biri dört 16 bitlik imzalanmamış tam sayıdan oluşuyorsa bunun yerine uint16x4 değerini kullanırsınız. Deseni görüyor musunuz?

Ardından offset, bu özelliğin tepe noktasında kaç bayt başladığını açıklar. Yalnızca arabelleğinizde, bu codelab'de karşınıza çıkacak birden fazla özellik varsa bu konuda endişelenmeniz gerekir.

Son olarak shaderLocation sizde var. Bu, 0 ile 15 arasında rastgele bir sayıdır ve tanımladığınız her özellik için benzersiz olmalıdır. Bu özelliği, bir sonraki bölümde öğreneceğiniz köşe gölgesindeki belirli bir girişe bağlar.

Bu değerleri şimdi tanımlasanız da aslında bu değerleri WebGPU API'sına henüz hiçbir yerde iletmediğinizi fark edeceksiniz. Bu sıralar yakında geliyor ancak en kolay yöntem, köşelerinizi tanımladığınız noktada düşünmektir. Bu nedenle, bunları daha sonra kullanmak üzere şu anda oluşturuyorsunuz.

Gölgelendiricilerle başlama

Oluşturmak istediğiniz veriler elinizde ancak GPU'ya bu verilerin tam olarak nasıl işleneceğini söylemeniz gerekiyor. Bunun büyük bir kısmı gölgelendiricilerde gerçekleşir.

Gölgelendiriciler, sizin yazdığınız ve GPU'nuzda yürütülen küçük programlardır. Her gölgelendirici, verilerin farklı bir aşamasında çalışır: Vertex işleme, Parça işleme veya genel İşlem. GPU'da oldukları için ortalama JavaScript'inizden daha katı bir şekilde yapılandırılırlar. Ama bu yapı, ekiplerin çok hızlı bir şekilde ve en önemlisi de buna paralel bir şekilde faaliyet göstermesini sağlar.

WebGPU'daki gölgelendiriciler, WGSL (WebGPU Gölgelendirme Dili) adı verilen bir gölgelendirme dilinde yazılır. WGSL, söz dizimsel olarak Rust'a benzer. Yaygın kullanılan GPU iş türlerini (vektör ve matris matematiği gibi) daha kolay ve hızlı hale getirmeyi amaçlayan özellikler içerir. Gölgelendirme dilinin bütününü öğretmek bu codelab'in kapsamı dışındadır, ancak birkaç basit örneği incelerken temel bilgilerin bazılarını öğreneceğinizi umuyoruz.

Gölgelendiriciler, WebGPU'ya dize olarak aktarılır.

  • Aşağıdakini vertexBufferLayout kodunun altındaki kodunuza kopyalayarak gölgelendirici kodunuzu gireceğiniz bir yer oluşturun:

index.html

const cellShaderModule = device.createShaderModule({
  label: "Cell shader",
  code: `
    // Your shader code will go here
  `
});

device.createShaderModule() adını verdiğiniz gölgelendiricileri oluşturmak için isteğe bağlı olarak label ve WGSL code değerlerini dize olarak sağlarsınız. (Çok satırlı dizelere izin vermek için burada vurgu işaretlerini kullandığınızı unutmayın.) Geçerli bir WGSL kodu eklediğinizde işlev, derlenen sonuçlarla birlikte bir GPUShaderModule nesnesi döndürür.

Köşe gölgelendiricisini tanımlama

Köşe gölgelendiriciyle başlayın çünkü GPU da burası burada başlıyor.

Köşe gölgelendirici, bir fonksiyon olarak tanımlanır ve GPU çağrıları, vertexBuffer etiketinizdeki her köşe için bir kez çalışır. vertexBuffer içinde altı konum (köşe) olduğundan tanımladığınız fonksiyon altı kez çağrılır. Her çağrıldığında, bağımsız değişken olarak işleve vertexBuffer parametresinden farklı bir konum iletilir ve bu, klip alanında karşılık gelen bir konumu döndürmek için köşe gölgelendirici işlevinin işidir.

Bunların da sıralı olarak çağrılmayabileceğini bilmeniz önemlidir. Bunun yerine GPU'lar, bunun gibi gölgelendiricileri paralel olarak çalıştırma konusunda uzmandır ve potansiyel olarak yüzlerce (hatta binlerce!) köşeyi aynı anda işler. Bu, GPU'ların olağanüstü hızının önemli bir parçası olsa da bazı kısıtlamalara da tabidir. Aşırı paralellik sağlamak için, tepe gölgelendiricileri birbirleriyle iletişim kuramaz. Her bir gölgelendirici çağrısı, aynı anda yalnızca tek bir tepe noktasına ait verileri görebilir ve yalnızca tek bir köşe noktası için değer üretebilir.

WGSL'de, bir tepe gölgelendirici işlevi istediğiniz gibi adlandırılabilir. Ancak hangi gölgelendirici aşamasını temsil ettiğinin belirtilmesi için bu işlevin önünde @vertex özelliği bulunmalıdır. WGSL, fn anahtar kelimesiyle işlevleri belirtir, bağımsız değişkenleri tanımlamak için parantez kullanır ve kapsamı tanımlamak için süslü ayraçlar kullanır.

  1. Aşağıdaki gibi boş bir @vertex işlevi oluşturun:

index.html (createShaderModule kodu)

@vertex
fn vertexMain() {

}

Ancak köşe gölgesindeki bir tepe noktasının en azından klip alanında işlenen köşenin son konumunu döndürmesi gerektiğinden bu durum geçerli değildir. Bu her zaman 4 boyutlu bir vektör olarak verilir. Vektörler gölgelendiricilerde çok yaygın bir şekilde kullanılır. Dolayısıyla, söz konusu dilde 4 boyutlu vektör için vec4f gibi kendi türlerine sahip birinci sınıf temel öğeler olarak kabul edilirler. 2D vektörler (vec2f) ve 3D vektörler (vec3f) için de benzer türler var!

  1. Döndürülen değerin gerekli konum olduğunu belirtmek için @builtin(position) özelliğiyle işaretleyin. -> simgesi, işlevin şunu döndürdüğünü belirtmek için kullanılır.

index.html (createShaderModule kodu)

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

}

Elbette, işlevin bir dönüş türü varsa işlevin gövdesinde bir değer döndürmeniz gerekir. vec4f(x, y, z, w) söz dizimini kullanarak döndürülecek yeni bir vec4f oluşturabilirsiniz. x, y ve z değerlerinin tümü kayan nokta sayılarıdır. Döndürülen değerde, tepe noktasının klip alanında nerede olduğunu belirtir.

  1. Statik bir (0, 0, 0, 1) değeri döndürdüğünüzde teknik olarak geçerli bir köşe gölgelendiriciniz olur. Bununla birlikte, GPU oluşturduğu üçgenlerin tek bir nokta olduğunu tespit edip sonrasında hiçbir şey göstermeyen bir köşe gölgeleyiciye sahip olursunuz.

index.html (createShaderModule kodu)

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

Bunun yerine, oluşturduğunuz arabellekteki verilerden yararlanmak istersiniz. Bunu, işleviniz için vertexBufferLayout içinde açıklamanızla eşleşen tür ve @location() özelliğine sahip bir bağımsız değişken tanımlayarak yapabilirsiniz. 0 değeri arasından shaderLocation değeri belirttiniz. Bu nedenle, WGSL kodunuzda bağımsız değişkeni @location(0) ile işaretleyin. Ayrıca biçimi, 2D bir vektör olan float32x2 olarak tanımladınız. Dolayısıyla WGSL'de bağımsız değişkeniniz bir vec2f. İstediğiniz adı verebilirsiniz ancak bunlar tepe konumlarınızı temsil ettiğinden pos gibi bir ad doğal görünür.

  1. Gölgelendirici işlevinizi aşağıdaki kodla değiştirin:

index.html (createShaderModule kodu)

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

Şimdi bu konumu döndürmeniz gerekiyor. Konum 2D vektör ve dönüş türü 4D vektör olduğundan, onu biraz değiştirmeniz gerekir. Yapmak istediğiniz işlem, konum bağımsız değişkenindeki iki bileşeni almak ve bunları dönüş vektörünün ilk iki bileşenine yerleştirmek, son iki bileşeni de sırasıyla 0 ve 1 olarak bırakmaktır.

  1. Hangi konum bileşenlerinin kullanılacağını açıkça belirterek doğru konumu döndürün:

index.html (createShaderModule kodu)

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

Bununla birlikte, bu tür eşlemeler gölgelendiricilerde çok yaygın olduğundan, konum vektörünü kullanışlı bir kısaltmayla ilk bağımsız değişken olarak da aktarabilirsiniz ve bu aynı anlama gelir.

  1. return ifadesini aşağıdaki kodla tekrar yazın:

index.html (createShaderModule kodu)

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

İşte bu, ilk köşe gölgelendiriciniz. Çok basit bir şeydir. Pozisyonu etkili bir şekilde değiştirmek gibidir, ancak başlamak için yeterince iyidir.

Parça gölgelendiriciyi tanımlama

Sırada parça gölgelendirici var. Parçalı gölgelendiriciler, köşe gölgelendiricilerine çok benzer bir şekilde çalışır, ancak her köşe için çağrılmaktan çok, çizilen her piksel için çağrılır.

Parça gölgelendiriciler, her zaman köşe gölgelendiricilerinden sonra çağrılır. GPU, köşe gölgelendiricilerinin çıkışını alır ve bu çıktıyı üç nokta haline getirir. Böylece, üç noktalı kümelerden üçgenler oluşturur. Daha sonra, çıkış rengi eklerinin hangi piksellerinin söz konusu üçgene dahil edildiğini belirleyerek bu üçgenlerin her birini rasterleştirir ve bu piksellerin her biri için parça gölgelendiriciyi bir kez çağırır. Parça gölgelendirici, genellikle köşe gölgelendiriciden gönderilen değerlerden ve GPU'nun renk ekine yazdığı dokular gibi öğelerden hesaplanan bir renk döndürür.

Köşe gölgelendiricilerinde olduğu gibi, parça gölgelendiriciler de son derece paralel bir şekilde yürütülür. Giriş ve çıkışlar açısından köşe gölgelendiricilerden biraz daha esnektirler, ancak her bir üçgenin her pikseli için bir renk döndüreceklerini düşünebilirsiniz.

WGSL parça gölgelendirici işlevi, @fragment özelliğiyle belirtilir ve aynı zamanda bir vec4f döndürür. Ancak bu durumda, vektör bir konumu değil, rengi temsil eder. Döndürülen rengin, beginRenderPass çağrısından hangi colorAttachment öğesine yazıldığını belirtmek için döndürülen değere bir @location özelliği verilmelidir. Yalnızca bir ekiniz olduğu için konum 0'dır.

  1. Aşağıdaki gibi boş bir @fragment işlevi oluşturun:

index.html (createShaderModule kodu)

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

}

Döndürülen vektörün dört bileşeni; kırmızı, yeşil, mavi ve alfa renk değerleridir. Bu değerler, daha önce beginRenderPass içinde ayarladığınız clearValue ile tam olarak aynı şekilde yorumlanır. vec4f(1, 0, 0, 1) parlak kırmızıdır. Kareniz için iyi bir renk gibi görünüyor. Yine de ürünü istediğiniz renge ayarlayabilirsiniz.

  1. Döndürülen renk vektörünü şu şekilde ayarlayın:

index.html (createShaderModule kodu)

@fragment
fn fragmentMain() -> @location(0) vec4f {
  return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}

Bu tam bir parça gölgelendiricidir! Çok da ilginç bir şey değil; her üçgenin her pikselini kırmızıya ayarlar, ancak bu şu an için yeterli.

Özetlemek gerekirse, yukarıda ayrıntılı olarak açıklanan gölgelendirici kodunu ekledikten sonra createShaderModule çağrınız şu şekilde görünür:

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

Oluşturma ardışık düzeni oluşturma

Gölgelendirici modülü, tek başına oluşturma için kullanılamaz. Bunun yerine, bunu device.createRenderPipeline() çağrısı yapılarak oluşturulan bir GPURenderPipeline'nın parçası olarak kullanmanız gerekir. Oluşturma ardışık düzeni, hangi gölgelendiricilerin kullanıldığı, köşe arabelleklerinde verilerin nasıl yorumlanacağı, ne tür geometri oluşturulması gerektiği (çizgiler, noktalar, üçgenler vb.) gibi şeyleri de içeren, geometrinin nasıl çizildiğini kontrol eder.

Oluşturma ardışık düzeni, tüm API'deki en karmaşık nesnedir ancak endişelenmeyin. İletebileceğiniz değerlerin çoğu isteğe bağlıdır ve başlamak için yalnızca birkaç değer sağlamanız gerekir.

  • Aşağıdaki gibi bir oluşturma ardışık düzeni oluşturun:

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

Her ardışık düzenin, ardışık düzenin ne tür girişlere (köşe tamponları dışında) ihtiyaç duyduğunu tanımlayan bir layout'ye ihtiyacı vardır ancak elinizde böyle bir giriş yoktur. Neyse ki şimdilik "auto" öğesini iletebilirsiniz. Ayrıca ardışık düzen, gölgelendiricilerden kendi düzenini derler.

Ardından, vertex aşamasıyla ilgili ayrıntıları sağlamanız gerekir. module, köşe gölgelendiricinizi içeren GPUShaderModule'dur. entryPoint ise her köşe gölgesi için çağrılan işlevin adını gölgelendirici kodunda verir. (Tek bir gölgelendirici modülünde birden çok @vertex ve @fragment işleviniz olabilir.) Arabellekler, verilerinizin bu ardışık düzeni birlikte kullandığınız köşe arabelleklerinde nasıl paketlendiğini açıklayan bir GPUVertexBufferLayout nesneleri dizisidir. Neyse ki vertexBufferLayout içinde bunu daha önce tanımlamıştınız. Burada devreye girersiniz.

Son olarak, fragment aşamasıyla ilgili ayrıntıları biliyorsunuz. Ayrıca, tepe noktası gibi bir gölgelendirici modülü ve EntryPoint de dahildir. Son adım, bu ardışık düzenin birlikte kullanıldığı targets öğesini tanımlamaktır. Bu, ardışık düzenin çıkışını sağlayan renk eklerinin ayrıntılarını (ör. format dokusu) veren sözlük dizisidir. Bu ayrıntıların, bu ardışık düzenin birlikte kullanıldığı tüm oluşturma geçişlerinin colorAttachments öğesinde verilen dokularla eşleşmesi gerekir. Oluşturma geçişiniz tuval bağlamındaki dokuları kullanır ve biçimi için canvasFormat içinde kaydettiğiniz değeri kullanır. Dolayısıyla burada da aynı biçimi iletirsiniz.

Bu, görüntü oluşturma ardışık düzeni oluştururken belirtebileceğiniz seçeneklerin tümüne yakın olmasa da bu codelab'in ihtiyaçları için yeterli olacaktır.

Kareyi çizme

Artık karenizi çizmek için ihtiyacınız olan her şeye sahipsiniz.

  1. Kareyi çizmek için tekrar encoder.beginRenderPass() ve pass.end() çağrı çiftine gidin, ardından şu yeni komutları aralarına ekleyin:

index.html

// After encoder.beginRenderPass()

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

// before pass.end()

Bu işlem, WebGPU'ya karenizi çizmek için gerekli tüm bilgileri sağlar. Öncelikle setPipeline() ile çizim için hangi ardışık düzenin kullanılması gerektiğini belirtirsiniz. Kullanılan gölgelendiriciler, köşe verilerinin düzeni ve diğer ilgili durum verileri buna dahildir.

Ardından, karenizin köşelerini içeren arabelleği kullanarak setVertexBuffer() öğesini çağırırsınız. Bu arabellek, mevcut ardışık düzenin vertex.buffers tanımındaki 0. öğeye karşılık geldiği için bu öğeyi 0 ile çağırırsınız.

Son olarak, daha önce yapılan tüm kurulumdan sonra garip bir şekilde basit görünen draw() çağrısını yapıyorsunuz. Aktarmanız gereken tek şey, oluşturulması gereken köşe sayısının sayısıdır. Bu sayı, halihazırda ayarlanmış köşe arabelleklerinden alınır ve halihazırda ayarlanmış olan ardışık düzen ile yorumlanır. Kodu 6 olarak doğrudan kodlayabilirsiniz, ancak köşe dizisinden (12 kayan nokta / köşe başına 2 koordinat == 6 köşe), kareyi örneğin bir daireyle değiştirmeye karar verdiyseniz, elle güncelleyeceğiniz daha az şey olacağı anlamına gelir.

  1. Ekranınızı yenileyin ve (nihayet) tüm emeğinizin sonuçlarını görün: büyük renkli bir kare.

WebGPU ile oluşturulmuş tek bir kırmızı kare

5. Izgara çizin

Öncelikle, kendinizi tebrik etmek için bir dakikanızı ayırın. Geometrinin ilk parçalarını ekranda görmek, genellikle çoğu GPU API'siyle ilgili en zor adımlardan biridir. Buradan yaptığınız tüm işlemler daha küçük adımlarla yapılabilir. Böylece, ilerledikçe ilerlemenizi kolayca doğrulayabilirsiniz.

Bu bölümde şunları öğreneceksiniz:

  • JavaScript'ten gölgelendiriciye değişkenler (üniforma denir) nasıl geçirilir?
  • Oluşturma davranışını değiştirmek için üniforma kullanma.
  • Aynı geometrinin birçok farklı varyantını çizmek için örneklem kullanımı

Izgarayı tanımlama

Izgara oluşturmak için, o tabloyla ilgili çok temel bir bilgiye sahip olmanız gerekir. Genişlik ve yükseklik olarak kaç hücre içeriyor? Bu, geliştirici olarak size bağlıdır, ancak işleri biraz kolaylaştırmak için ızgarayı kare olarak (aynı genişlik ve yükseklikte) kullanın ve boyutun ikinin üssü olan bir boyut kullanın. (Bu sayede ilerideki matematik işlemleri kolaylaşabilir.) Biraz zaman çizelgesini büyütmek istiyorsunuz ama bu bölümün geri kalanında ızgara boyutunu 4x4 olarak ayarlamanız daha kolay olacak. Daha sonra ölçeği artırın!

  • JavaScript kodunuzun en üstüne bir sabit değer ekleyerek ızgara boyutunu tanımlayın.

index.html

const GRID_SIZE = 4;

Ardından, zemine GRID_SIZE kere GRID_SIZE sığdırmak için karenizi oluşturma yönteminizi güncellemeniz gerekir. Bu, karenin çok daha küçük olması ve çok sayıda resim içermesi gerektiği anlamına gelir.

Bunu yapmanın bir yolu, köşe arabelleğinizi önemli ölçüde büyütmek ve içinde doğru boyut ve konumda karelerin GRID_SIZE kat GRID_SIZE değerinde kareyi tanımlamaktır. Hatta bunun kodu çok fena olmazdı. Birkaç döngü ve biraz matematik. Ancak bu aynı zamanda GPU'dan en iyi şekilde yararlanması ve efektin uygulanması için gerekenden fazla bellek kullanılması anlamına gelmez. Bu bölümde, daha GPU uyumlu bir yaklaşım anlatılmaktadır.

Tek tip arabellek oluşturma

Öncelikle, seçtiğiniz ızgara boyutunu gölgelendiriciye iletmeniz gerekir, çünkü bu boyut, nesnelerin görünümünü değiştirmek için bunu kullanır. Boyutu gölgelendiriciye sabit bir şekilde kodlayabilirsiniz, ancak bu durumda ızgara boyutunu değiştirmek istediğinizde gölgelendiriciyi yeniden oluşturmanız ve ardışık düzen oluşturmanız gerekir ve bu da pahalıdır. Daha iyi bir yol, ızgara boyutunu gölgelendiriciye üniforma olarak sağlamaktır.

Daha önce, köşe gölgelendiricinin her çağrısına köşe tamponundan farklı bir değerin iletildiğini öğrenmiştiniz. Tek tip, her çağrı için aynı olan bir tampon değeridir. Bunlar, bir geometri parçasında (ör. konumu), tam bir animasyon karesinde (geçerli zaman gibi) veya uygulamanın tüm kullanım ömrü boyunca (ör. kullanıcı tercihi) ortak olan değerleri iletmek için yararlıdır.

  • Aşağıdaki kodu ekleyerek tek tip bir arabellek oluşturun:

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

Bu kod daha önce köşe arabelleğini oluşturmak için kullandığınız kodla neredeyse aynı olduğundan size çok tanıdık gelecektir. Bunun nedeni, üniformaların WebGPU API'ye köşeleriyle aynı GPUBuffer nesneleri üzerinden aktarılmasıdır. Başlıca fark, usage süresinin bu sefer GPUBufferUsage.VERTEX yerine GPUBufferUsage.UNIFORM içermesidir.

Gölgelendiricide üniformalara erişme

  • Aşağıdaki kodu ekleyerek bir tek tip tanımlayın:

index.html (createShaderModule çağrısı)

// 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 

Bu, gölgelendiricinizde grid adlı bir tekdüzen tanımlar. Bu, az önce tek tip arabelleğe kopyaladığınız diziyle eşleşen 2D kayan vektördür. Ayrıca formanın @group(0) ve @binding(0) için bağlı olduğunu da belirtir. Bu değerlerin ne anlama geldiğini birazdan öğreneceksiniz.

Ardından, gölgelendirici kodunun başka bir yerinde ızgara vektörünü istediğiniz gibi kullanabilirsiniz. Bu kodda tepe noktası konumunu ızgara vektörüne bölersiniz. pos 2D bir vektör ve grid bir 2D vektör olduğu için, WGSL bileşene göre bir bölme gerçekleştirir. Yani sonuç, vec2f(pos.x / grid.x, pos.y / grid.y) demekle aynıdır.

Bu tür vektör işlemleri, birçok oluşturma ve hesaplama tekniğinde gerekli olduğundan GPU gölgelendiricilerinde çok yaygın olarak kullanılır.

Sizin durumunuzda bu, (ızgara boyutu 4 ise) oluşturduğunuz karenin orijinal boyutunun dörtte biri olacağı anlamına gelir. Bunlardan dördünü bir satıra veya sütuna sığdırmak istiyorsanız bu mükemmel bir seçenektir.

Bağlama Grubu Oluşturma

Gölgelendiricide açık üniforma, oluşturduğunuz tamponla bağlanmaz. Bunun için bir bağlama grubu oluşturup ayarlamanız gerekir.

Bağlama grubu, gölgelendiriciniz için aynı anda erişilebilir hale getirmek istediğiniz kaynakların oluşturduğu bir koleksiyondur. Tek tip arabellek gibi çeşitli arabellek türlerinin yanı sıra burada ele alınmayan ancak WebGPU oluşturma tekniklerinin yaygın parçaları olan dokular ve örnekleyiciler gibi diğer kaynakları içerebilir.

  • Tek tip arabellek ve oluşturma ardışık düzeni oluşturulduktan sonra aşağıdaki kodu ekleyerek tek tip arabellekinizle bir bağlama grubu oluşturun:

index.html

const bindGroup = device.createBindGroup({
  label: "Cell renderer bind group",
  layout: cellPipeline.getBindGroupLayout(0),
  entries: [{
    binding: 0,
    resource: { buffer: uniformBuffer }
  }],
});

Artık standart olan label'ınıza ek olarak, bu bağlama grubunun ne tür kaynaklar içerdiğini açıklayan bir layout da gereklidir. Bu, ilerideki bir adımda daha detaylı inceleyeceğiniz bir konudur. Ancak, ardışık düzeni layout: "auto" ile oluşturduğunuz için şimdilik ardışık düzeninizden bağlama grubu düzenini isteyebilirsiniz. Bu, ardışık düzenin gölgelendirici kodunun kendisinde bildirdiğiniz bağlamalardan otomatik olarak bağlantı grubu düzenleri oluşturmasına neden olur. Bu durumda, getBindGroupLayout(0) isteğinde bulunursunuz. Buradaki 0, gölgelendiriciye yazdığınız @group(0) öğesine karşılık gelir.

Düzeni belirttikten sonra bir entries dizisi sağlarsınız. Her giriş, en azından aşağıdaki değerleri içeren bir sözlüktür:

  • binding, gölgelendiriciye girdiğiniz @binding() değerine karşılık gelir. Bu durumda, 0.
  • resource, belirtilen bağlama dizininde değişkene gösterilmesini istediğiniz gerçek kaynaktır. Bu durumda tek tip tamponunuz olur.

İşlev, opak ve sabit bir herkese açık kullanıcı adı olan GPUBindGroup değerini döndürür. Bir bağlama grubunun işaret ettiği kaynakları, oluşturulduktan sonra değiştiremezsiniz ancak bu kaynakların içeriğini değiştirebilirsiniz. Örneğin, tek tip arabelleği yeni bir ızgara boyutu içerecek şekilde değiştirirseniz bu, bağlama grubunu kullanan gelecekteki çizim çağrılarına yansır.

Bağlama grubunu bağlama

Artık bağlama grubu oluşturulduğuna göre, yine de WebGPU'ya çizim sırasında bunu kullanmasını söylemeniz gerekir. Neyse ki bu oldukça basit.

  1. Oluşturma kartına geri dönün ve bu yeni satırı draw() yönteminden önce ekleyin:

index.html

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

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

pass.draw(vertices.length / 2);

İlk bağımsız değişken olarak iletilen 0, gölgelendirici kodundaki @group(0) öğesine karşılık gelir. @group(0) kapsamındaki her @binding öğesinin bu bağlama grubundaki kaynakları kullandığını söylüyorsunuz.

Artık tek tip arabellek, gölgelendiricinize gösterilir.

  1. Sayfanızı yenilediğinizde şuna benzer bir sayfa görürsünüz:

Koyu mavi arka planın ortasında küçük kırmızı bir kare.

Yaşasın! Kareniz artık önceki boyutunun dörtte biri oldu! Bu, çok fazla bir şey olmasa da formanızın aslında uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.

Gölgelendiricide geometriyi manipüle etme

Artık gölgelendiricide ızgara boyutuna başvuruda bulunabildiğinize göre, oluşturduğunuz geometriyi istediğiniz ızgara kalıbına uyacak şekilde değiştirmek için birtakım çalışmalar yapmaya başlayabilirsiniz. Bunun için tam olarak neyi başarmak istediğinizi düşünün.

Tuvalinizi kavramsal olarak ayrı hücrelere bölmeniz gerekiyor. Siz sağa doğru hareket ettikçe X ekseninin, yukarı doğru hareket ettikçe Y ekseninin de artma kuralını korumak için, ilk hücrenin tuvalin sol alt köşesinde yer aldığını varsayalım. Mevcut kare geometriniz ortada olmak üzere aşağıdaki gibi bir düzen elde etmenizi sağlar:

Normalleştirilmiş Cihaz Koordinatı alanı, merkezinde oluşturulmuş kare geometriyle her hücre görselleştirilirken bölünecek kavramsal tabloyu temsil ediyor.

Göreviniz, gölgelendiricide, kare geometriyi hücre koordinatlarını verilen hücrelerden herhangi birine yerleştirmenize olanak tanıyan bir yöntem bulmaktır.

Öncelikle, karenizin tuvalin merkezini çevreleyecek şekilde tanımlandığından hiçbir hücreyle düzgün bir şekilde hizalanmadığını görebilirsiniz. Karenin içinde güzelce hizalanması için hücrenin yarım hücrelik kaydırılmasını istersiniz.

Bunu düzeltmenin bir yolu, karenin köşe tamponunu güncellemektir. Köşeleri, sağ alt köşeyi (-0,8, -0,8) yerine (0,1, 0,1) olacak şekilde kaydırdığınızda, bu kareyi hücre sınırlarıyla daha iyi hizalanacak şekilde taşıyabilirsiniz. Ancak, köşelerin gölgelendiricinizde nasıl işleneceği üzerinde tam kontrol sahibi olduğunuzdan, gölgelendirici kodunu kullanarak bunları doğru yere sürüklemek de aynı derecede kolaydır.

  1. Aşağıdaki kodu kullanarak tepe gölgelendirici modülünü değiştirin:

index.html (createShaderModule çağrısı)

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

Bu işlem, ızgara boyutuna bölmeden önce her köşeyi yukarı ve sağa bir kaydırma yapar (bunun, klip alanının yarısı olduğunu unutmayın). Sonuç, başlangıç noktasının hemen dışında, ızgaraya hizalı olarak güzel bir kare elde etmenizi sağlar.

Kanvasın kavramsal olarak 4x4&#39;lük bir ızgaraya bölünmüş, hücrede kırmızı kare (2, 2) bulunan bir görselleştirmesi

Ardından, tuvalinizin koordinat sistemi ortaya (0, 0) ve sol alt kısma (-1, -1) yerleştirildiğinden ve (0, 0) sol altta olmasını istediğinizden, geometrik konumunuzu ızgara boyutuna (-1, -1) sonra bölüp o köşeye taşımanız gerekir.

  1. Geometrinizin konumunu şu şekilde çevirin:

index.html (createShaderModule çağrısı)

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

Şimdi kareniz (0, 0) hücresinde güzel bir şekilde konumlandırılmış!

Kanvasın kavramsal olarak hücrede kırmızı kare (0, 0) bulunan 4x4&#39;lük bir tabloya bölünmüş hali

Farklı bir hücreye yerleştirmek istiyorsanız ne olur? Gölgelendiricinizde bir cell vektörü tanımlayıp bu vektörü let cell = vec2f(1, 1) gibi statik bir değerle doldurarak bulabilirsiniz.

Bunu gridPos öğesine eklerseniz algoritmadaki - 1 işlemi geri alınır, dolayısıyla istediğiniz işlem bu olmaz. Bunun yerine, kareyi her hücre için yalnızca bir ızgara birimi (tuvalin dörtte biri) taşımak istiyorsunuz. grid değerine tekrar bölmeniz gerekiyor.

  1. Izgara konumunu aşağıdaki gibi değiştirin:

index.html (createShaderModule çağrısı)

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

Şimdi yenilerseniz aşağıdakileri görürsünüz:

Tuvalin kavramsal olarak 4x4&#39;lük bir tabloya bölünmüş ve hücre (0, 0), hücre (0, 1), hücre (1, 0) ve hücre (1, 1) arasında ortalanmış kırmızı bir kare bulunan görseli

Hımm. Tam olarak istediğiniz gibi olmadı.

Bunun nedeni, tuval koordinatlarının -1'den +1'e gittiği için aslında 2 birim olmasıdır. Yani kanvasın dörtte birini tuvalin üzerine taşımak istiyorsanız 0, 5 birim taşımanız gerekir. Bu, GPU koordinatlarıyla akıl yürütürken kolayca yapılabilecek bir hatadır! Neyse ki sorunun çözümü de aynı derecede kolay.

  1. Ofseti 2 ile çarpın. Örneğin:

index.html (createShaderModule çağrısı)

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

Böylece tam olarak istediğiniz şeyi elde etmiş olursunuz.

Kanvasın kavramsal olarak 4x4&#39;lük bir ızgaraya bölünmüş, hücrede kırmızı kare (1, 1) bulunan bir görselleştirmesi

Ekran görüntüsü şuna benzer:

Koyu mavi arka plan üzerindeki kırmızı karenin ekran görüntüsü. Önceki şemada açıklandığı gibi, ızgara yer paylaşımı olmadan çizilen kırmızı kare.

Dahası, artık cell öğesini ızgara sınırları içindeki herhangi bir değere ayarlayabilir ve sonra karenin istenen konumda oluşturulmasını görmek için sayfayı yenileyebilirsiniz.

Örnek çizme

Artık biraz matematik kullanarak kareyi istediğiniz yere yerleştirebileceğinize göre sonraki adım, ızgaranın her bir hücresinde bir kare oluşturmaktır.

Bunun bir yolu, tek tip bir arabelleğe hücre koordinatlarını yazmak ve ardından ızgaradaki her kare için bir kez draw çağrısı yapmak ve üniformayı her seferinde güncellemektir. Ancak bu çok yavaş olur çünkü GPU'nun her seferinde JavaScript tarafından yeni koordinatın yazılmasını beklemesi gerekir. GPU'dan iyi performans elde etmenin anahtarlarından biri, sistemin diğer kısımlarında beklerken geçirdiği süreyi en aza indirmektir.

Bunun yerine, etkileme adı verilen bir teknik kullanabilirsiniz. Örnekleme, GPU'ya tek bir draw çağrısıyla aynı geometrinin birden fazla kopyasını çizmesini söylemenin bir yoludur. Bu, her kopya için bir kez draw çağrısı yapmaktan çok daha hızlıdır. Geometrinin her kopyası bir örnek olarak adlandırılır.

  1. GPU'ya karenizin yeterli sayıda örneğini doldurmasını istediğinizi bildirmek için mevcut çizim çağrınıza bir bağımsız değişken ekleyin:

index.html

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

Bu, sisteme karenizin altı (vertices.length / 2) köşe noktasını 16 (GRID_SIZE * GRID_SIZE) kez çizmesini istediğinizi belirtir. Ancak sayfayı yenilerseniz aşağıdakileri görmeye devam edersiniz:

Hiçbir şeyin değişmediğini belirtmek için önceki şemayla aynı olan resim.

Neden? Çünkü bu karelerin 16'sını da aynı noktada çizersiniz. Gölgelendiricide, geometriyi örnek bazında yeniden konumlandıran bazı ek mantığa ihtiyacınız vardır.

Gölgelendiricide, köşe arabelleğinizden gelen pos gibi köşe özelliklerine ek olarak, WGSL'nin yerleşik değerleri olarak bilinen özelliklere de erişebilirsiniz. Bunlar, WebGPU tarafından hesaplanan değerlerdir ve bu değerlerden biri instance_index'dir. instance_index, gölgelendirici mantığınızın bir parçası olarak kullanabileceğiniz, 0 ile number of instances - 1 arasındaki imzalanmamış 32 bitlik bir sayıdır. Bu değer, aynı örneğin parçası olan işlenen her köşe noktası için aynıdır. Bu, tepe gölgelendiricinizin, köşe tamponunuzdaki her konum için bir kez olmak üzere, 0 instance_index değeri ile altı kez çağrıldığı anlamına gelir. Ardından instance_index 1 ile altı kez daha, ardından instance_index tutarında 2 ile altı kez daha.

Bunun nasıl çalıştığını görmek için gölgelendirici girişlerinize yerleşik instance_index eklemeniz gerekir. Bunu konum ile aynı şekilde yapın, ancak @location özelliğiyle etiketlemek yerine @builtin(instance_index) özelliğini kullanın ve ardından bağımsız değişkeni istediğiniz gibi adlandırın. (Örnek kodla eşleşmesi için bunu instance olarak adlandırabilirsiniz.) Ardından, bunu gölgelendirici mantığının bir parçası olarak kullanın.

  1. Hücre koordinatlarının yerine instance kullanın:

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

Şimdi sayfayı yenilediğinizde gerçekten birden fazla karenizin olduğunu görürsünüz! Ancak 16 önerinin hepsini göremezsiniz.

Sol alt köşeden sağ üst köşeye doğru lacivert arka plan üzerinde çapraz bir çizgi içinde dört kırmızı kare.

Bunun nedeni, oluşturduğunuz hücre koordinatlarının (0, 0), (1, 1), (2, 2)... (15, 15) değerine kadar ayarlanmış olmasıdır. Ancak, bunlardan yalnızca ilk dördü tuvale sığar. İstediğiniz ızgarayı yapmak için instance_index öğesini, her bir dizin ızgaranızdaki benzersiz bir hücreyle eşlenecek şekilde dönüştürmeniz gerekir. Örneğin:

Tuvalin kavramsal olarak 4x4&#39;lük bir tabloya bölünmüş ve her hücrenin aynı zamanda doğrusal bir örnek dizinine karşılık gelen bir görselleştirmesi.

Bunun matematiği oldukça basittir. Her hücrenin X değeri için instance_index modülünü ve ızgara genişliğini istersiniz. Bunu, WGSL'de % operatörüyle gerçekleştirebilirsiniz. Her hücrenin Y değeri için instance_index değerinin ızgara genişliğine bölünmesiyle elde edilir ve tüm kesirli kalanlar silinir. Bunu, WGSL'nin floor() işleviyle yapabilirsiniz.

  1. Hesaplamaları şu şekilde değiştirebilirsiniz:

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

Kodda bu güncellemeyi yaptıktan sonra, uzun süredir beklenen kareler ızgarasına nihayet sahipsiniz.

Koyu mavi arka plan üzerinde dört sütunlu kırmızı karelerden oluşan dört satır.

  1. Çalıştığına göre geri dönüp ızgara boyutunu kıvırın.

index.html

const GRID_SIZE = 32;

Koyu mavi arka plan üzerinde 32 sütundan oluşan 32 kırmızı kare.

İşte oldu! Bu ızgarayı artık gerçekten ama gerçekten büyük hale getirebilirsiniz ve ortalama GPU'nuz bunu gayet iyi işliyor. GPU performansı sorunlarıyla karşılaşmadan çok önce tek kareleri görmezsiniz.

6. Ekstra kredi: Daha renkli hale getirin!

Codelab'in geri kalanı için temel hazırlığı yaptığınız için bu noktada kolayca bir sonraki bölüme geçebilirsiniz. Aynı rengi paylaşan karelerden oluşan ızgaradan oluşan ızgara düzeninden yararlanılabilse de hiç heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla her şeyi biraz daha parlak hale getirebilirsiniz!

Gölgelendiricilerde struct kullanma

Şimdiye kadar köşe gölgelendiriciden bir veri parçası aktardınız: dönüştürülen konum. Ancak, köşe gölgelendiricisinden çok daha fazla veri döndürebilir ve bunu parça gölgelendiricide kullanabilirsiniz.

Köşe gölgelendiricisinden veri iletmenin tek yolu, verinin döndürülmesidir. Bir konum döndürmek için köşe gölgelendirici her zaman gereklidir. Dolayısıyla, tepeyle birlikte başka veriler de döndürmek isterseniz onu bir struct içine yerleştirmeniz gerekir. WGSL'deki yapılar, bir veya daha fazla adlandırılmış özellik içeren nesne türleridir. Tesisler, @builtin ve @location gibi özelliklerle de işaretlenebilir. Bunları işlevlerin dışında tanımlarsınız. Daha sonra, bunların örneklerini gerektiğinde işlevlerin içine ve dışına aktarabilirsiniz. Örneğin, mevcut köşe gölgelendiricinizi düşünün:

index.html (createShaderModule çağrısı)

@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);
}
  • İşlev girişi ve çıkışı için struct'ları kullanarak aynı şeyi ifade edin:

index.html (createShaderModule çağrısı)

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

Bunun için input ile giriş konumuna ve örnek dizinine başvurmanız gerektiğini ve önce döndürdüğünüz struct'ın değişken olarak bildirilmesi ve bağımsız özelliklerinin ayarlanması gerektiğini unutmayın. Bu durumda çok fazla fark yaratmaz ve aslında gölgelendirici işlevini biraz daha uzun hale getirir, ancak gölgelendiricileriniz daha karmaşık hale geldikçe, struct'ları kullanmak verilerinizi düzenlemenize yardımcı olabilir.

Köşe ve parça işlevleri arasında veri iletme

@fragment fonksiyonunuzun mümkün olduğunca basit olduğunu hatırlatmak isteriz:

index.html (createShaderModule çağrısı)

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

Giriş almıyorsunuz ve çıktınız olarak düz bir renk (kırmızı) gönderiyorsunuz. Gölgelendirici, renklendirdiği geometri hakkında daha fazla bilgi sahibiyse bu ekstra veriyi kullanarak işleri biraz daha ilginç hale getirebilirsiniz. Örneğin, her karenin rengini hücre koordinatına göre değiştirmek isterseniz ne yapabilirsiniz? @vertex aşaması, hangi hücrenin oluşturulduğunu bilir; @fragment aşamasına iletmeniz yeterlidir.

Köşe noktası ve parça aşamaları arasında veri aktarmak için bu verileri tercih ettiğimiz @location ile bir çıkış struct'a dahil etmeniz gerekir. Hücre koordinatını aktarmak istediğiniz için, bunu önceki VertexOutput struct'a ekleyin ve geri dönmeden önce @vertex işlevinde ayarlayın.

  1. Köşe gölgelendiricinizin döndürülen değerini şu şekilde değiştirin:

index.html (createShaderModule çağrısı)

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 işlevinde, aynı @location değerini içeren bir bağımsız değişken ekleyerek değeri alın. (Adların eşleşmesi gerekmez, ancak eşleşirlerse işleri takip etmek daha kolay olur!)

index.html (createShaderModule çağrısı)

@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. Alternatif olarak, bunun yerine bir struct kullanabilirsiniz:

index.html (createShaderModule çağrısı)

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

@fragment
fn fragmentMain(input: FragInput) -> @location(0) vec4f {
  return vec4f(input.cell, 0, 1);
}
  1. Başka bir alternatif de kodunuzda bu işlevlerin ikisi de aynı gölgelendirici modülünde tanımlandığından @vertex aşamasının çıkış struct'ını yeniden kullanmaktır. Adlar ve konumlar doğal olarak tutarlı olduğundan değerlerin iletilmesini kolaylaştırır.

index.html (createShaderModule çağrısı)

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

Hangi kalıbı seçerseniz seçin sonuç olarak @fragment işlevindeki hücre numarasına erişebilir ve rengi etkilemek için onu kullanabilirsiniz. Yukarıdaki kodlardan herhangi biriyle sonuç şöyle görünür:

En soldaki sütunun yeşil, alt satırın kırmızı, diğer tüm karelerin sarı olduğu bir kare ızgarası.

Artık kesinlikle daha fazla renk var, ancak görünüşü çok hoş değil. Neden yalnızca sol ve alt satırların farklı olduğunu merak ediyor olabilirsiniz. Bunun nedeni, @fragment işlevinden döndürdüğünüz renk değerlerinin her bir kanalın 0 ila 1 aralığında olmasını beklemesi ve bu aralığın dışındaki tüm değerlerin bu değere sabitlenmesidir. Öte yandan, hücre değerleriniz her bir eksende 0 ile 32 arasında değişir. Burada gördüğünüz gibi, ilk satır ve sütun hemen kırmızı veya yeşil renk kanalında 1 tam değere ulaşır ve ondan sonraki her hücre aynı değere sabitlenir.

Renkler arasında daha yumuşak bir geçiş istiyorsanız her renk kanalı için bir kesir değeri döndürmeniz gerekir. Bu değer, ideal olarak sıfırdan başlayıp her eksende bir ile biter. Diğer bir deyişle, grid sayısına bir diğerini bölersiniz.

  1. Parça gölgelendiriciyi şu şekilde değiştirin:

index.html (createShaderModule çağrısı)

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

Sayfayı yenilediğinizde yeni kodun tüm ızgarada daha hoş bir renk gradyanı verdiğini görebilirsiniz.

Farklı köşelerinde siyahtan kırmızıya, yeşile ve sarıya değişen karelerden oluşan bir ızgara.

Bu kesinlikle bir gelişme olsa da şimdi sol alt tarafta ızgaranın siyah hale geldiği talihsiz bir karanlık köşe var. Hayat Oyunu simülasyonunu yapmaya başladığınızda, çizelgenin görülmesi zor bir bölümü oyunun durumunu gizleyecektir. Konuyu aydınlatabilmem güzel olurdu.

Neyse ki, henüz kullanmadığınız, mavi renk için kullanabileceğiniz bir renk kanalınız var. İdeal olarak istediğiniz efekt, diğer renklerin en koyu olduğu yerde mavinin en parlak olmasını, ardından diğer renkler yoğunlaştıkça sönmesini sağlar. Bunu yapmanın en kolay yolu, kanalın 1'de başlamasını ve hücre değerlerinden birini çıkarmasını sağlamaktır. c.x veya c.y olabilir. Her ikisini de deneyin, ardından tercih ettiğiniz seçeneği seçin.

  1. Parça gölgelendiriciye daha parlak renkler ekleyin. Örneğin:

createShaderModule çağrısı

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

Sonuç oldukça iyi görünüyor.

Farklı köşelerinde kırmızıdan yeşile, maviye ve sarıya değişen karelerden oluşan bir ızgara.

Bu önemli bir adım değildir. Ancak daha iyi göründüğü için ilgili kontrol noktası kaynak dosyasına eklenmiştir. codelab'deki diğer ekran görüntüleri ise bu daha renkli ızgarayı yansıtır.

7. Hücre durumunu yönetme

Ardından, GPU'da depolanan duruma göre ızgarada hangi hücrelerin oluşturulacağını kontrol etmeniz gerekir. Bu son simülasyon için önemlidir!

Tüm ihtiyacınız olan, her hücre için bir açma/kapatma sinyalidir. Bu nedenle, neredeyse her değer türünde geniş bir diziyi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun tek tip tamponlar için de başka bir kullanım alanı olduğunu düşünebilirsiniz. Tek tip arabelleklerin boyutu sınırlı olduğu, dinamik olarak boyutlandırılmış dizileri (gölgecide dizi boyutunu belirtmeniz gerekir) desteklemediği ve hesaplama gölgelendiricileri tarafından yazamadığı için bu çalışmayı yapabilirsiniz ancak bu daha zordur. En sorunlu son öğe de bu, çünkü bir Compute gölgelendiricide GPU'da Yaşam Oyunu simülasyonunu yapmak istiyorsunuz.

Neyse ki tüm bu sınırlamalardan kaçınan başka bir tampon seçeneği vardır.

Depolama arabelleği oluşturma

Depolama arabellekleri, işlem gölgelendiricilerinde okunup yazılabilen ve köşe gölgelendiricilerinde okunabilen genel kullanım arabellekleridir. Bunlar çok büyük olabilir ve gölgelendiricide tanımlanmış belirli bir boyuta ihtiyaç duymazlar. Bu da genel belleğe benzer. Hücre durumunu depolamak için bu yöntemi kullanırsınız.

  1. Hücre durumunuza ilişkin bir depolama arabelleği oluşturmak için, şimdiye kadar muhtemelen arabellek oluşturma kodunun tanıdık görünümlü bir snippet'e dönüşecek şeyi kullanın:

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

Köşe noktası ve tek tip arabelleklerde olduğu gibi, device.createBuffer() öğesini uygun boyutla çağırın. Ardından, bu kez GPUBufferUsage.STORAGE kullanımını belirttiğinizden emin olun.

Arabelleği, aynı boyuttaki TypedArray'i değerlerle doldurup device.queue.writeBuffer() yöntemini çağırarak önceki gibi doldurabilirsiniz. Tamponunuzun ızgara üzerindeki etkisini görmek istediğiniz için öncelikle onu tahmin edilebilir bir şeyle doldurun.

  1. Her üçüncü hücreyi aşağıdaki kodla etkinleştirin:

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

Gölgelendiricideki depolama arabelleğini okuma

Ardından, ızgarayı oluşturmadan önce depolama arabelleğinin içeriğine bakmak için gölgelendiricinizi güncelleyin. Bu, daha önce eklenen üniformalara çok benziyor.

  1. Gölgenizi aşağıdaki kodla güncelleyin:

index.html

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

İlk olarak, ızgara üniformunun hemen altına yerleştirilen bağlama noktasını eklersiniz. grid formasıyla aynı @group değerini kullanmak istiyorsunuz ancak @binding sayısının farklı olması gerekiyor. var türü, farklı tampon türünü yansıtmak amacıyla storage şeklindedir ve tek bir vektör yerine cellState için sağladığınız tür, JavaScript'te Uint32Array ile eşleştirmek üzere u32 değerlerinden oluşan bir dizidir.

Daha sonra, @vertex fonksiyonunuzun gövdesinde hücrenin durumunu sorgulayın. Durum, depolama arabelleğinde düz bir dizide depolandığından, geçerli hücrenin değerini aramak için instance_index öğesini kullanabilirsiniz.

Bir hücrenin etkin olmadığı belirtilmişse hücreyi nasıl kapatırsınız? Diziden aldığınız etkin ve etkin olmayan durumlar 1 veya 0 olduğundan, geometriyi etkin duruma göre ölçeklendirebilirsiniz. 1 ölçeğinde ölçekleme, geometriyi yalnız bırakır, 0'a kadar ölçeklendirme ise geometrinin tek bir nokta haline gelmesine neden olur ve daha sonra GPU, şekli siler.

  1. Konumu hücrenin etkin durumuna göre ölçeklendirmek için gölgelendirici kodunuzu güncelleyin. WGSL'nin tür güvenliği şartlarını karşılamak için durum değeri bir f32 olarak ayarlanmalıdır:

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

Depolama arabelleğini bağlama grubuna ekleme

Hücre durumunun geçerlilik kazanması için önce depolama arabelleğini bir bağlama grubuna ekleyin. Tek tip arabellekle aynı @group öğesinin parçası olduğundan, bunu JavaScript kodunda da aynı bağlama grubuna ekleyin.

  • Depolama arabelleğini aşağıdaki gibi ekleyin:

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

Yeni girişe ait binding değerinin, gölgelendiricideki karşılık gelen değerin @binding() ile eşleştiğinden emin olun.

Bunu yaptıktan sonra, şablonu yenileyebilir ve ızgarada görebilirsiniz.

Lacivert arka plan üzerinde sol alttan sağ üste giden renkli karelerin çapraz çizgileri.

Ping pong arabellek kalıbını kullanma

Oluşturduğunuz simülasyon gibi çoğu simülasyon, genellikle durumlarının en az iki kopyasını kullanır. Simülasyonun her adımında, durumun bir kopyasını okur ve diğerine yazarlar. Ardından, bir sonraki adımda yazıyı çevirin ve daha önce yazdığı durumu okuyun. Eyaletin en güncel sürümü her adımdaki eyalet kopyaları arasında gidip geldiğinden bu duruma genellikle ping pong kalıbı denir.

Bu neden gerekli? Basitleştirilmiş bir örneğe bakın: Aktif blokları her adımda bir hücre sağa taşıdığınız çok basit bir simülasyon yazdığınızı düşünün. Konunun kolayca anlaşılmasını sağlamak için verilerinizi ve simülasyonunuzu JavaScript'te tanımlarsınız:

// 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.

Ancak bu kodu çalıştırırsanız etkin hücre tek adımda dizinin sonuna kadar gider. Neden? Durumu yerinde güncellemeye devam ettiğiniz için, etkin hücreyi sağa taşıyıp bir sonraki hücreye bakıyorsunuz ve... hey! Etkin! Tekrar sağa taşısam iyi olur. Verileri gözlemlediğiniz anda değiştirmeniz, sonuçları bozar.

Pinpon kalıbını kullanarak yalnızca son adımın sonuçlarını kullanarak simülasyonun bir sonraki adımını her zaman uygulamanızı sağlarsınız.

// 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. Birbirinin aynısı olan iki arabellek oluşturmak için ayrılmış depolama alanı arabelleğinizi güncelleyerek kendi kodunuzda bu kalıbı kullanın:

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. İki tampon arasındaki farkı görselleştirmeye yardımcı olması için bunları farklı verilerle doldurun:

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. Oluşturma işleminizde farklı depolama arabelleklerini göstermek için bağlama gruplarınızı da iki farklı varyant içerecek şekilde güncelleyin:

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

Oluşturma döngüsü ayarlama

Şu ana kadar sayfa yenileme başına yalnızca bir çizim işlemi gerçekleştirdiniz, ancak şimdi zaman içinde güncellenen verileri göstermek istiyorsunuz. Bunun için basit bir oluşturma döngüsüne ihtiyacınız vardır.

Oluşturma döngüsü, içeriğinizi belirli bir aralıkla zemine çeken, sonsuz sayıda tekrar eden bir döngüdür. Animasyon eklemek isteyen çoğu oyun ve diğer içerik, geri çağırmaları ekranın yenilendiği hızda (saniyede 60 kez) planlamak için requestAnimationFrame() işlevini kullanır.

Bu uygulama bu özelliği de kullanabilir. Ancak bu durumda, simülasyonun yaptığı işlemleri daha kolay takip edebilmek için güncellemelerin daha uzun adımlar halinde gerçekleşmesini isteyebilirsiniz. Simülasyonunuzun güncelleme hızını kontrol edebilmek için döngüyü kendiniz yönetebilirsiniz.

  1. İlk olarak, simülasyonumuzun güncellenmesi için bir hız seçin (200 ms iyidir, ancak isterseniz daha yavaş veya daha hızlı ilerleyebilirsiniz) ve ardından simülasyon işleminin kaç adımının tamamlandığını takip edin.

index.html

const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
  1. Ardından, oluşturma işlemi için şu anda kullandığınız kodun tamamını yeni bir işleve taşıyın. setInterval() ile bu işlevi istediğiniz aralıkta tekrarlanacak şekilde programlayın. İşlevin, adım sayısını da güncellediğinden emin olun ve bunu kullanarak iki bağlama grubundan hangisinin bağlanacağını seçin.

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

Şimdi uygulamayı çalıştırdığınızda tuval, oluşturduğunuz iki durum arabelleğini gösterirken geri ve ileri hareket ediyor.

Lacivert arka plan üzerinde sol alttan sağ üste giden renkli karelerin çapraz çizgileri. Koyu mavi arka plan üzerinde renkli karelerden oluşan dikey şeritler.

Böylece, işlerin oluşturma kısmını bitirmiş sayılırsınız. Bir sonraki adımda oluşturduğunuz Game of Life simülasyonunun çıkışını görüntülemeye hazırsınız. Bu adımda nihayet hesaplama gölgelendiricilerini kullanmaya başlayacaksınız.

WebGPU'nun oluşturma yeteneklerinde, burada keşfettiğiniz küçük parçadan çok daha fazlasının olduğu açıktır ancak geri kalanlar bu codelab'in kapsamı dışındadır. Umarız bu video, WebGPU'da oluşturma işleminin nasıl çalıştığına dair yeterince bilgi verir ve 3D oluşturma gibi daha gelişmiş teknikleri keşfetmeyi kolaylaştırır.

8. Simülasyonu çalıştırma

Şimdi, yapbozun son önemli parçası: Bir bilgisayar gölgesinde Game of Life simülasyonunu gerçekleştirmek var!

Sonunda işlem gölgelendiricilerini kullanın!

Bu codelab'de hesaplama gölgelendiricileri hakkında soyut bilgiler edindiniz. Peki bunlar tam olarak nedir?

Compute gölgelendiriciler, GPU'da aşırı paralellikle çalışacak şekilde tasarlanmalarından dolayı köşe ve parça gölgelendiricilere benzer, ancak diğer iki gölgelendirici aşamasından farklı olarak belirli bir giriş ve çıkış grubu yoktur. Yalnızca depolama arabellekleri gibi seçtiğiniz kaynaklardan gelen verileri okuyor ve yazıyorsunuz. Bu, her köşe, örnek veya piksel için bir kez yürütmek yerine, gölgelendirici işlevinin kaç çağrısı istediğinizi belirtmeniz gerektiği anlamına gelir. Ardından, gölgelendiriciyi çalıştırdığınızda hangi çağrının işlendiği size bildirilir ve hangi verilere erişeceğinize ve buradan hangi işlemleri yapacağınıza karar verebilirsiniz.

Compute gölgelendiriciler, tıpkı köşe ve parça gölgelendiriciler gibi, bir gölgelendirici modülünde oluşturulmalıdır. Başlamak için bunu kodunuza ekleyin. Tahmin edebileceğiniz gibi, uyguladığınız diğer gölgelendiricilerin yapısına göre, Compute gölgelendiricinizin ana işlevinin @compute özelliğiyle işaretlenmesi gerekir.

  1. Aşağıdaki kodla bir Compute gölgelendirici oluşturun:

index.html

// Create the compute shader that will process the simulation.
const simulationShaderModule = device.createShaderModule({
  label: "Game of Life simulation shader",
  code: `
    @compute
    fn computeMain() {

    }`
});

3D grafikler için sıklıkla GPU'lar kullanıldığından, işlem gölgelendiricileri gölgelendiricinin X, Y ve Z eksenlerinde belirli sayıda çağrılmasını isteyebilirsiniz. Bu sayede, 2D veya 3D ızgaraya uygun işleri kolayca dağıtabilirsiniz. Bu özellik kullanım alanınız için çok idealdir. Bu gölgelendiriciyi, simülasyonunuzun her hücresi için bir kez olmak üzere GRID_SIZE kez GRID_SIZE kez çağırmak istiyorsunuz.

GPU donanımı mimarisinin yapısı nedeniyle bu ızgara çalışma gruplarına ayrılmıştır. Bir çalışma grubunun X, Y ve Z boyutları vardır. Her bir boyutun 1 boyutu olsa da, çalışma gruplarınızı biraz daha büyütmenin genellikle performans açısından avantajları vardır. Gölgelendiriciniz için 8x8 oranında rastgele bir çalışma grubu boyutu seçin. Bu, JavaScript kodunuzu izlemek açısından yararlıdır.

  1. Çalışma grubunuzun boyutu için şu şekilde bir sabit değer tanımlayın:

index.html

const WORKGROUP_SIZE = 8;

Ayrıca, çalışma grubu boyutunu gölgelendirici işlevine de eklemeniz gerekir. Bunu JavaScript şablon sabit değerlerini kullanarak yaparsınız. Böylece, biraz önce tanımladığınız sabit değeri kolayca kullanabilirsiniz.

  1. Çalışma grubu boyutunu aşağıdaki gibi gölgelendirici işlevine ekleyin:

index.html (Compute createShaderModule çağrısı)

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

}

Bu, gölgelendiriciye bu işlevle yapılan çalışmanın (8 x 8 x 1) gruplarda yapıldığını bildirir. (En azından X eksenini belirtmeniz gerekse de, bıraktığınız tüm eksenler varsayılan olarak 1'e ayarlanır.)

Diğer gölgelendirici aşamalarında olduğu gibi, hangi çağrıda olduğunuzu belirtmek ve yapmanız gereken işe karar vermek üzere işlem gölgelendirici işlevinize girdi olarak kabul edebileceğiniz çeşitli @builtin değerleri vardır.

  1. @builtin değeri ekleyin. Örneğin:

index.html (Compute createShaderModule çağrısı)

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

}

Gölgelendirici çağrıları ızgarasında nerede olduğunuzu bildiren, imzalanmamış tam sayıların üç boyutlu bir vektörü olan yerleşik global_invocation_id öğesini geçersiniz. Bu gölgelendiriciyi, tablonuzdaki her hücre için bir kez çalıştırırsınız. (0, 0, 0), (1, 0, 0), (1, 1, 0) gibi sayılar var. Bunların tamamı (31, 31, 0)'a kadar var. Bu da onları çalışacağınız hücre dizini olarak değerlendirebileceğiniz anlamına geliyor!

Compute gölgelendiriciler, köşe ve parça gölgelendiricilerde olduğu gibi üniformalardan da yararlanabilir.

  1. Izgara boyutunu aşağıdaki gibi göstermek için Compute gölgelendiricinizle birlikte bir tek tip kullanın:

index.html (Compute createShaderModule çağrısı)

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

}

Köşe gölgelendirmesinde olduğu gibi, hücre durumunu depolama tamponu olarak da gösterirsiniz. Ama bu durumda bunlardan ikisine ihtiyacınız olacak. Compute gölgelendiricileri, köşe konumu veya parça rengi gibi gerekli bir çıkışa sahip olmadığından, Compute gölgelendiriciden sonuç almanın tek yolu değerleri depolama arabelleğine veya dokuya yazmaktır. Daha önce öğrendiğiniz masa tenisi yöntemini kullanın; tablonun geçerli durumunda beslenen bir depolama arabelleği ve ızgaranın yeni durumunu yazdığınız bir depolama arabelleğiniz olur.

  1. Hücre girişini ve çıkış durumunu şu şekilde depolama arabelleği olarak kullanıma sunun:

index.html (Compute createShaderModule çağrısı)

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

}

İlk depolama arabelleğinin var<storage> ile tanımlandığını ve böylece salt okunur olduğunu, ikinci depolama arabelleğinin ise var<storage, read_write> ile tanımlandığını unutmayın. Böylece arabelleği hem okuma hem de yazma işlemleri gerçekleştirebilir ve arabelleği, Compute gölgelendiricinizin çıkışı olarak kullanabilirsiniz. (WebGPU'da salt yazma depolama modu yoktur).

Ayrıca, hücre dizininizi doğrusal depolama dizisiyle eşleştirebileceğiniz bir yönteme sahip olmanız gerekir. Bu, temel olarak köşe gölgelendiricide yaptığınızın tam tersi. instance_index doğrusal biçimini alıp bir 2D ızgara hücresiyle eşlediniz. (Bunun için algoritmanızın vec2f(i % grid.x, floor(i / grid.x)) olduğunu hatırlatmak isteriz.)

  1. Diğer yönde giden bir fonksiyon yazın. Hücrenin Y değerini alır, ızgara genişliğiyle çarpar ve ardından hücrenin X değerini ekler.

index.html (Compute createShaderModule çağrısı)

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

Son olarak, çalışıp çalışmadığını görmek için çok basit bir algoritma uygulayın: Bir hücre açıksa kapanır veya tam tersi olur. Henüz Hayat Oyunu değil, ancak bilgi işlem gölgesinin çalıştığını göstermek için yeterli.

  1. Aşağıdaki gibi basit bir algoritma ekleyin:

index.html (Compute createShaderModule çağrısı)

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

Compute gölgelendiriciniz için bu kadar. Şimdilik bu kadar! Ancak sonuçları görebilmeniz için yapmanız gereken birkaç değişiklik daha var.

Bağlama Grubu ve Ardışık Düzenleri Kullanma

Yukarıdaki gölgelendiricide, oluşturma ardışık düzeninizle büyük oranda aynı girişlerin (üniformalar ve depolama arabellekleri) kullanıldığını fark edebilirsiniz. Yani aynı bağlama gruplarını kullanıp bunlarla işin bittiğini düşünebilirsiniz, değil mi? Neyse ki bu mümkündür. Bunu yapabilmek için biraz daha manuel kurulum gerekiyor.

Her bağlama grubu oluşturduğunuzda bir GPUBindGroupLayout sağlamanız gerekir. Daha önce, bu düzeni, oluşturma ardışık düzeninde getBindGroupLayout() çağırarak elde ediyordunuz. Buna karşılık, düzeni oluştururken layout: "auto" sağlayan düzen otomatik olarak oluşturuluyordu. Bu yaklaşım, yalnızca tek bir ardışık düzen kullandığınızda da işe yarar. Ancak kaynakları paylaşmak isteyen birden fazla ardışık düzeniniz varsa bu düzeni açık bir şekilde oluşturmanız ve ardından hem bağlama grubuna hem de ardışık düzenlere sağlamanız gerekir.

Nedenini anlamak için şunu düşünün: oluşturma ardışık düzenlerinizde tek bir tek tip arabellek ve bir depolama arabelleği kullanırsınız. Ancak az önce yazdığınız Compute gölgelendiricide ikinci bir depolama arabelleğine ihtiyacınız vardır. İki gölgelendirici, tek tip ve ilk depolama arabelleği için aynı @binding değerlerini kullandığından, bunları ardışık düzenler arasında paylaşabilirsiniz ve oluşturma ardışık düzeni, kullanmadığı ikinci depolama arabelleğini yok sayar. Yalnızca belirli bir ardışık düzen tarafından kullanılanları değil, bağlama grubunda bulunan tüm kaynakları açıklayan bir düzen oluşturmak istiyorsunuz.

  1. Bu düzeni oluşturmak için device.createBindGroupLayout() komutunu çağırın:

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

Bu, entries listesini açıklamanız açısından bağlama grubunun kendisini oluşturmaya benzer. Aradaki fark, kaynağın kendisini sağlamak yerine girişin ne tür bir kaynak olması gerektiğini ve nasıl kullanıldığını açıklamanızdır.

Her girişte kaynak için binding numarasını verirsiniz. Bu numara, (bağlama grubunu oluşturduğunuzda öğrendiğiniz gibi) gölgelendiricilerdeki @binding değeriyle eşleşir. Ayrıca, hangi gölgelendirici aşamalarının kaynağı kullanabileceğini gösteren GPUShaderStage işaretleri olan visibility bilgisini de sağlarsınız. Hem tek tip hem de ilk depolama arabelleğinin tepe noktası ve işlem gölgelendiricilerinde erişilebilir olmasını istiyorsunuz ancak ikinci depolama arabelleğinin yalnızca işlem gölgelendiricilerinde erişilebilir olması gerekir.

Son olarak da ne tür bir kaynağın kullanıldığını belirtirsiniz. Bu, neyi göstermeniz gerektiğine bağlı olarak farklı bir sözlük anahtarıdır. Burada üç kaynak da tampon olduğundan her biri için seçenekleri tanımlamak üzere buffer tuşunu kullanırsınız. texture veya sampler gibi seçenekler diğer seçenekler arasındadır ancak bunlara burada ihtiyacınız yoktur.

Arabellek sözlüğünde type arabellek kullanımı gibi seçenekleri ayarlarsınız. Varsayılan değer "uniform" olduğundan, 0 bağlantısı için sözlüğü boş bırakabilirsiniz. (Ancak girişin arabellek olarak tanımlanması için en azından buffer: {} değerini ayarlamanız gerekir.) Gölgelendiricide read_write erişimiyle kullanmadığınız için bağlama 1'e "read-only-storage" türü verilir. 2. bağlama ise read_write erişimiyle kullandığınız için "storage" türüne sahiptir.

bindGroupLayout oluşturulduktan sonra, bağlama grubunu ardışık düzenden sorgulamak yerine bağlama gruplarınızı oluştururken aktarabilirsiniz. Bunu yapmanız, az önce tanımladığınız düzenle eşleşmesi için her bir bağlama grubuna yeni bir depolama arabellek girişi eklemeniz gerektiği anlamına gelir.

  1. Bağlama grubu oluşturma işlemini şu şekilde güncelleyin:

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

Bağlama grubu bu açık bağlama grubu düzenini kullanacak şekilde güncellendi. Artık aynı şeyi kullanmak için oluşturma ardışık düzenini güncellemeniz gerekir.

  1. Bir GPUPipelineLayout oluşturun.

index.html

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

Ardışık düzen düzeni, bir veya daha fazla ardışık düzenin kullandığı bağlama grubu düzenlerinin (bu durumda var) listesidir. Dizideki bağlama grubu düzenlerinin sırası, gölgelendiricilerdeki @group özelliklerine karşılık gelmelidir. (Yani bindGroupLayout, @group(0) ile ilişkilendirilmiş.)

  1. Ardışık düzen düzenini edindikten sonra, "auto" yerine bu ardışık düzeni kullanmak için ardışık düzeni güncelleyin.

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

Compute ardışık düzenini oluşturma

Köşe ve parça gölgelendiricilerinizi kullanmak için bir oluşturma ardışık düzenine ihtiyaç duyduğunuz gibi, Compute gölgelendiricinizi de kullanmak için bir işlem ardışık düzenine ihtiyacınız vardır. Neyse ki işlem ardışık düzenleri, ayarlanacak bir durumu olmadığından yalnızca gölgelendirici ve düzene sahip olduğundan oluşturma ardışık düzenlerine göre çok daha karmaşıktır.

  • Aşağıdaki kodla bir Compute ardışık düzeni oluşturun:

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

Güncellenen oluşturma ardışık düzeninde olduğu gibi "auto" yerine yeni pipelineLayout öğesini geçirdiğinize dikkat edin. Bu, hem oluşturma ardışık düzeninizin hem de işlem ardışık düzeninizin aynı bağlama gruplarını kullanabilmesini sağlar.

İşlem kartları

Böylece bilgi işlem ardışık düzeninden gerçekten yararlanabilirsiniz. Oluşturma işlemini bir oluşturma geçişinde yaptığınız düşünüldüğünde, muhtemelen Compute Pass'te işlem yapmanız gerektiğini tahmin edebilirsiniz. Hem işlem hem de oluşturma işleri aynı komut kodlayıcıda gerçekleşebileceği için updateGrid işlevinizi biraz karıştırmanız gerekir.

  1. Kodlayıcı oluşturmayı işlevin üst kısmına taşıyın ve ardından bununla birlikte (step++ öncesinde) bir işlem kartı başlatın.

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...

Tıpkı işlem ardışık düzenleri gibi, işlem geçişlerini de oluşturma işlemlerine kıyasla çok daha kolay başlatabilirsiniz çünkü eklerle ilgili endişelenmeniz gerekmez.

Oluşturma geçişinin işlem kartındaki en son sonuçları hemen kullanmasına olanak tanıdığı için işlem geçirmeyi oluşturma işleminden önce yapmak istiyorsunuz. İşlem ardışık düzeninin çıkış arabelleğinin, oluşturma ardışık düzeninin giriş arabelleği hâline gelmesi için geçişler arasında step sayısını artırmanızın nedeni de budur.

  1. Ardından, ardışık düzen ve bağlama grubunu bilgi işlem aktarımı içinde ayarlayın. Aynı kalıbı kullanarak bağlama grupları arasında geçiş yaparken de oluşturma geçişi için kullandığınız kalıbı kullanın.

index.html

const computePass = encoder.beginComputePass();

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

computePass.end();
  1. Son olarak, oluşturma geçişini olduğu gibi çizmek yerine, çalışmayı bilgi işlem gölgelendiricisine göndererek her eksende kaç çalışma grubu yürütmek istediğinizi belirtirsiniz.

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

Burada çok önemli dikkat edilmesi gereken bir nokta da dispatchWorkgroups() ilettiğiniz sayının çağrı sayısı olmadığıdır. Bunun yerine, gölgelendiricinizdeki @workgroup_size ile tanımlanan yürütülecek çalışma grubu sayısıdır.

Gölgelendiricinin tüm ızgarayı kaplayacak şekilde 32x32 kez yürütülmesini istiyorsanız ve çalışma grubunuzun boyutu 8x8 ise, 4x4 çalışma gruplarını dağıtmanız gerekir (4 * 8 = 32). Bu nedenle ızgara boyutunu çalışma grubu boyutuna böler ve bu değeri dispatchWorkgroups() aralığına aktarabilirsiniz.

Şimdi sayfayı tekrar yenileyebilirsiniz. Her güncellemede ızgaranın kendisini ters çevirdiğini görürsünüz.

Lacivert arka plan üzerinde sol alttan sağ üste giden renkli karelerin çapraz çizgileri. Koyu mavi bir arka plan üzerinde soldan sağa doğru giden iki kare şeklinde çapraz çizgili renkli kareler. Önceki resmin tersi.

Hayat Oyunu algoritmasını uygulama

Son algoritmayı uygulamak üzere Compute gölgelendiriciyi güncellemeden önce, depolama arabelleği içeriğini başlatan koda geri dönmek ve her sayfa yüklemesinde rastgele bir arabellek oluşturmak üzere bunu güncellemek istersiniz. (Normal kalıplar çok ilginç Hayat Oyunu başlangıç noktaları sağlamaz.) Değerleri istediğiniz gibi rastgele hale getirebilirsiniz, ancak makul sonuçlar veren kolay bir başlangıç yolu vardır.

  1. Her hücreyi rastgele durumda başlatmak için cellStateArray ilk kullanıma hazırlama işlemini aşağıdaki kodla güncelleyin:

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

Artık Hayat Oyunu simülasyonunun mantığını uygulayabilirsiniz. Buraya ulaşmak için gereken her şeyden sonra gölgelendirici kodu hayal kırıklığına uğratacak kadar basit olabilir!

Öncelikle, herhangi bir hücre için kaç komşusunun etkin olduğunu bilmeniz gerekir. Hangilerinin etkin olduğu önemli değil, yalnızca sayısı önemli.

  1. Komşu hücre verilerine daha kolay ulaşmak için verilen koordinatın cellStateIn değerini döndüren bir cellActive işlevi ekleyin.

index.html (Compute createShaderModule çağrısı)

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

cellActive işlevi, hücre etkinse bir değer döndürür. Dolayısıyla, etrafındaki sekiz hücrenin tamamı için cellActive çağrısının döndürdüğü değer, kaç komşu hücrenin etkin olduğunu elde etmenizi sağlar.

  1. Aşağıdaki gibi etkin komşuların sayısını bulun:

index.html (Compute createShaderModule çağrısı)

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

Fakat bu küçük bir soruna yol açar: Kontrol ettiğiniz hücre panoyun dışında kaldığında ne olur? Şu anki cellIndex() mantığınıza göre sonraki veya önceki satıra taşıyor ya da arabelleğin kenarından doğru gidiyor.

Hayat Oyunu'nda bunu çözmenin yaygın ve kolay bir yolu, ızgaranın kenarındaki hücrelerin ızgaranın karşı kenarındaki hücreleri komşuları olarak ele alması ve böylece bir tür sarma etkisi yaratmaktır.

  1. cellIndex() işlevinde küçük bir değişiklikle birlikte ızgaranın çevrelenmesi desteği.

index.html (Compute createShaderModule çağrısı)

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

X ve Y hücrelerini ızgara boyutundan fazla uzattığında sarmalamak için % operatörünü kullandığınızda depolama arabellek sınırlarının dışına hiçbir zaman erişmemenizi sağlarsınız. Bu sayede, activeNeighbors sayısının tahmin edilebilir olduğundan emin olabilirsiniz.

Ardından dört kuraldan birini uygularsınız:

  • İkiden az komşusu olan hücreler devre dışı kalır.
  • İki veya üç komşusu olan etkin hücreler etkin kalır.
  • Tam olarak üç komşusu olan etkin olmayan hücreler etkin hale gelir.
  • Üçten fazla komşusu olan hücreler devre dışı kalır.

Bunu bir dizi "if" ifadesiyle yapabilirsiniz ancak WGSL, bu mantığa uygun olan "geçiş" ifadelerini de destekler.

  1. Hayat Oyunu mantığını şu şekilde uygulayın:

index.html (Compute createShaderModule çağrısı)

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

Referans olması amacıyla, son Compute shader modülü çağrısı şu şekilde görünür:

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

Hepsi bu kadar! Hepsi bu kadar! Sayfanızı yenileyin ve yeni oluşturulan hücresel otomatınızın büyümesini izleyin.

Koyu mavi arka plan üzerinde oluşturulmuş renkli hücrelerin yer aldığı, Hayat Oyunu simülasyonundan örnek durumun ekran görüntüsü.

9. Tebrikler!

WebGPU API'yi kullanarak klasik Conway'in Hayat Oyunu simülasyonunun tamamen GPU'nuzda çalışan bir sürümünü oluşturdunuz.

Sırada ne var?

Daha fazla bilgi

Referans belgeler