İlk WebGPU uygulamanız

İlk WebGPU uygulamanız

Bu codelab hakkında

subjectSon güncelleme Tem 17, 2025
account_circleYazan: Brandon Jones, François Beaufort

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şmek için kullanılan yeni ve modern bir API'dir.

Modern API

WebGPU'dan önce, WebGPU'nun özelliklerinin bir alt kümesini sunan WebGL vardı. Bu teknoloji, yeni bir zengin web içeriği sınıfının ortaya çıkmasını sağladı ve geliştiriciler bu teknolojiyle harika şeyler yarattı. Ancak bu API, 2007'de yayınlanan ve daha da eski OpenGL API'sine dayanan OpenGL ES 2.0 API'sine dayanıyordu. GPU'lar bu süre zarfında önemli ölçüde gelişti ve bunlarla arayüz oluşturmak için kullanılan yerel API'ler de Direct3D 12, Metal ve Vulkan ile birlikte gelişti.

WebGPU, bu modern API'lerin avantajlarını web platformuna getirir. Bu API, GPU özelliklerini platformlar arası bir şekilde etkinleştirmeye odaklanırken web'de doğal bir his veren ve üzerine kurulu olduğu 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 da bu konuda farklı değildir. Hem masaüstü hem de mobil GPU'larda günümüzün en popüler oluşturma tekniklerinin çoğunu desteklemek için gereken özelliklere sahiptir ve donanım özellikleri gelişmeye devam ettikçe gelecekte yeni özelliklerin eklenmesine olanak tanır.

Bilgi işlem

WebGPU, oluşturmanın yanı sıra genel amaçlı ve yüksek düzeyde paralel iş yüklerini gerçekleştirmek için GPU'nuzun potansiyelini ortaya çıkarır. Bu hesaplama 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 edilmiş parçası olarak kullanılabilir.

Bu codelab'de, basit bir giriş projesi oluşturmak için WebGPU'nun hem oluşturma hem de hesaplama özelliklerinden nasıl yararlanacağınızı öğreneceksiniz.

Ne oluşturacaksınız?

Bu codelab'de, WebGPU kullanarak Conway'in Yaşam Oyunu'nu oluşturacaksınız. 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 hesaplama özelliklerini kullanın.

Bu codelab'in nihai ürününün ekran görüntüsü

Yaşam Oyunu, bir dizi kurala göre zaman içinde durum değiştiren bir hücre ızgarasının bulunduğu hücresel otomat olarak bilinir. Yaşam Oyunu'nda hücreler, komşu hücrelerinden kaçının etkin olduğuna bağlı olarak etkin veya devre dışı hale gelir. Bu da izlerken değişen ilginç desenlere yol açar.

Neler öğreneceksiniz?

  • WebGPU'yu ayarlama ve tuvali yapılandırma
  • Basit 2D geometri nasıl çizilir?
  • Çizilenleri değiştirmek için köşe ve parça gölgelendiricileri kullanma
  • Basit bir simülasyon gerçekleştirmek için hesaplama gölgelendiricilerini kullanma.

Bu codelab, WebGPU'nun temel kavramlarını tanıtmaya odaklanmaktadır. Bu doküman, API'nin kapsamlı bir incelemesi olarak tasarlanmamıştır ve 3D matris matematiği gibi sıklıkla ilgili olan konuları kapsamaz (veya gerektirmez).

Gerekenler

  • ChromeOS, macOS veya Windows'da Chrome'un yeni bir sürümü (113 ya da sonraki sürümler). WebGPU, tarayıcılar ve platformlar arası bir API'dir 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'leri hakkında bilgi sahibi olmanız gerekmez. Ancak bu API'lerle ilgili deneyiminiz varsa WebGPU ile birçok benzerlik olduğunu fark edebilirsiniz. Bu benzerlikler, öğrenme sürecinize hızlı bir başlangıç yapmanıza yardımcı olabilir.

2. Hazırlanın

Kodu alma

Bu codelab'in herhangi bir bağımlılığı yoktur ve 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. Ancak, kontrol noktası olarak kullanılabilecek bazı çalışan örnekleri https://github.com/GoogleChromeLabs/your-first-webgpu-app-codelab adresinde bulabilirsiniz. Bu kaynaklara göz atabilir ve takıldığınız noktalarda bunlardan yararlanabilirsiniz.

Geliştirici Konsolu'nu kullanın.

WebGPU, uygun kullanımı zorunlu kılan birçok kurala sahip oldukça karmaşık bir API'dir. Daha da kötüsü, API'nin çalışma şekli nedeniyle birçok hata için normal JavaScript istisnaları oluşturamaz. Bu da sorunun tam olarak nereden kaynaklandığını belirlemeyi zorlaştırır.

WebGPU ile geliştirme yaparken, özellikle de yeni başlıyorsanız sorunlarla karşılaşmanız kaçınılmazdır. API'nin geliştiricileri, GPU geliştirme ile çalışmanın zorluklarının farkındadır ve WebGPU kodunuzun herhangi bir hataya neden olması durumunda, geliştirici konsolunda sorunu belirlemenize ve düzeltmenize yardımcı olacak çok ayrıntılı ve faydalı mesajlar almanızı sağlamak için çok çalışmıştır.

Herhangi bir web uygulamasında çalışırken konsolu açık tutmak her zaman faydalıdır ancak bu durum özellikle burada geçerlidir.

3. WebGPU'yu başlatma

<canvas> ile başlayın

WebGPU, yalnızca hesaplama yapmak için kullanılmak isteniyorsa ekranda herhangi bir şey göstermeden kullanılabilir. Ancak codelab'de yapacağımız gibi bir şey oluşturmak istiyorsanız tuval kullanmanız gerekir. Bu nedenle, başlamak için iyi bir nokta.

İçinde tek bir <canvas> öğesi bulunan yeni bir HTML dokümanı oluşturun. Ayrıca, tuval öğesine sorgu gönderdiğimiz bir <script> etiketi de oluşturun. (Veya 00-starter-page.html dosyasını kullanın.)

  • Aşağıdaki kodu kullanarak 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 isteğinde bulunma

Artık WebGPU'nun ayrıntılarına girebilirsiniz. Öncelikle WebGPU gibi API'lerin tüm web ekosistemine yayılmasının biraz zaman alabileceğini göz önünde bulundurmanız gerekir. Bu nedenle, ilk önleyici adım olarak kullanıcının tarayıcısının WebGPU'yu kullanıp kullanamadığını kontrol etmek iyi bir fikirdir.

  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, sayfanın WebGPU kullanmayan bir moda geri dönmesini sağlayarak WebGPU kullanılamıyorsa kullanıcıyı bilgilendirmeniz gerekir. (Belki bunun yerine WebGL kullanılabilir?) Ancak bu codelab'in amacı doğrultusunda, kodun daha fazla yürütülmesini durdurmak için yalnızca bir hata atarsınız.

WebGPU'nun tarayıcı tarafından desteklendiğini öğrendikten sonra, uygulamanız için WebGPU'yu başlatmanın ilk adımı GPUAdapter istemektir. Adaptörü, cihazınızdaki belirli bir GPU donanımının WebGPU'daki temsili olarak düşünebilirsiniz.

  1. Bağdaştırıcı almak için navigator.gpu.requestAdapter() yöntemini kullanın. Bir söz döndürdüğünden, await ile çağrılması en uygun yöntemdir.

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ığı ele almanız gerekir. Bu durum, kullanıcının tarayıcısı WebGPU'yu destekliyorsa ancak GPU donanımında WebGPU'yu kullanmak için gereken tüm özellikler yoksa meydana gelebilir.

Çoğu zaman, tarayıcının varsayılan bir bağdaştırıcı seçmesine izin vermek (burada yaptığınız gibi) yeterlidir. Ancak daha gelişmiş ihtiyaçlar için requestAdapter() öğesine iletebileceğiniz bağımsız değişkenler vardır. Bu bağımsız değişkenler, birden fazla GPU'ya sahip cihazlarda (ör. bazı dizüstü bilgisayarlar) düşük güç veya yüksek performanslı donanım kullanmak isteyip istemediğinizi belirtir.

Bir bağdaştırıcınız olduğunda GPU ile çalışmaya başlamadan önceki son adım GPUDevice istemektir. Cihaz, GPU ile etkileşimin en çok gerçekleştiği ana arayüzdür.

  1. adapter.requestDevice() numaralı telefonu arayarak cihazı alın. Bu işlem, bir söz de döndürür.

index.html

const device = await adapter.requestDevice();

requestAdapter()'da olduğu gibi, belirli donanım özelliklerini etkinleştirme veya daha yüksek sınırlar isteme gibi daha gelişmiş kullanımlar için geçirilebilecek seçenekler vardır ancak varsayılanlar sizin amaçlarınız için yeterlidir.

Canvas'ı yapılandırma

Artık bir cihazınız olduğuna göre, sayfada bir şeyler göstermek için kullanmak istiyorsanız yapmanız gereken bir şey daha var: Tuvali, yeni oluşturduğunuz cihazla kullanılacak şekilde yapılandırmak.

  • Bunu yapmak için önce canvas.getContext("webgpu") işlevini çağırarak tuvalden GPUCanvasContext isteyin. (Bu, sırasıyla 2d ve webgl bağlam türlerini kullanarak Canvas 2D veya WebGL bağlamlarını başlatmak için kullandığınız aynı çağrıdır.) Döndürdüğü context, configure() yöntemi kullanılarak cihazla ilişkilendirilmelidir. Örneğin:

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 önemlileri, bağlamı kullanacağınız device ve bağlamın kullanması gereken doku biçimi olan format'dir.

Dokular, WebGPU'nun resim verilerini depolamak için kullandığı nesnelerdir ve her dokunun, GPU'nun bu verilerin bellekte nasıl düzenlendiğini bilmesini sağlayan bir biçimi vardır. Doku belleğinin işleyiş şekliyle ilgili ayrıntılar bu codelab'in kapsamı dışındadır. Bilmeniz gereken önemli nokta, tuval bağlamının kodunuzun içine çizmesi için dokular sağladığı ve kullandığınız biçimin, tuvalin bu resimleri ne kadar verimli bir şekilde gösterebileceği üzerinde etkili olabileceğidir. Farklı cihaz türleri, farklı doku biçimleri kullanıldığında en iyi performansı gösterir. Cihazın tercih ettiği biçimi kullanmazsanız resim sayfanın bir parçası olarak gösterilmeden önce arka planda fazladan bellek kopyaları oluşturulabilir.

Neyse ki WebGPU, tuvaliniz için hangi biçimi kullanmanız gerektiğini size bildirdiği için bunların hiçbiri hakkında endişelenmenize gerek yok. Neredeyse tüm durumlarda, yukarıda gösterildiği gibi navigator.gpu.getPreferredCanvasFormat() çağrılarak döndürülen değeri iletmek istersiniz.

Tuvali temizleme

Artık bir cihazınız olduğuna ve tuval bu cihazla yapılandırıldığına göre tuvalin içeriğini değiştirmek için cihazı kullanmaya başlayabilirsiniz. Başlamak için arka planı düz bir renkle temizleyin.

Bunu veya WebGPU'daki diğer işlemleri yapabilmek için GPU'ya ne yapması gerektiğini söyleyen bazı komutlar sağlamanız gerekir.

  1. Bunu yapmak için cihazın, GPU komutlarını kaydetmek için bir arayüz sağlayan GPUCommandEncoder oluşturmasını sağlayın.

index.html

const encoder = device.createCommandEncoder();

GPU'ya göndermek istediğiniz komutlar oluşturmayla (bu durumda tuvali temizleme) ilgili olduğundan bir sonraki adım, oluşturma geçişi başlatmak için encoder kullanmaktır.

Render geçişleri, WebGPU'daki tüm çizim işlemlerinin gerçekleştiği zamanlardır. Her biri, gerçekleştirilen tüm ç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 depolama veya kenarları yumuşatma gibi çeşitli amaçlara yönelik ekler adı verilen birkaç doku sağlayabilir. Ancak bu uygulama için yalnızca bir tane gerekir.

  1. context.getCurrentTexture() işlevini çağırarak daha önce oluşturduğunuz tuval bağlamından dokuyu alın. Bu işlev, tuvalin width ve height özellikleriyle eşleşen piksel genişliği ve yüksekliğine sahip bir doku döndürür. Ayrıca, context.configure() işlevini çağırdığınızda belirtilen format değerini de 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. Render geçişleri, dokunun hangi kısımlarının oluşturulacağını belirten bir GPUTexture yerine GPUTextureView sağlamanızı gerektirir. Bu durum yalnızca daha gelişmiş kullanım alanlarında önemlidir. Bu nedenle, burada doku üzerinde herhangi bir bağımsız değişken olmadan createView() çağrısı yaparsınız. Bu, oluşturma geçişinin dokunun tamamını kullanmasını istediğinizi gösterir.

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

  • loadOp değeri "clear" ise oluşturma geçişi başladığında dokunun temizlenmesini istediğinizi gösterir.
  • storeOp değeri "store" olduğunda, 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ğiniz belirtilir.

Render geçişi başladıktan sonra hiçbir şey yapmayın. En azından şimdilik. Oluşturma geçişini loadOp: "clear" ile başlatmak, doku görünümünü ve tuvali temizlemek için yeterlidir.

  1. Aşağıdaki çağrıyı beginRenderPass()'dan hemen sonra ekleyerek oluşturma geçişini sonlandırın:

index.html

pass.end();

Bu çağrıları yapmanın GPU'nun herhangi bir işlem yapmasına neden olmadığını bilmek önemlidir. Bunlar, GPU'nun daha sonra yapması için kaydedilen komutlardır.

  1. GPUCommandBuffer oluşturmak için komut kodlayıcıda finish() işlevini çağırın. Komut arabelleği, kaydedilen komutların opak bir tutamacıdır.

index.html

const commandBuffer = encoder.finish();
  1. queue GPUDevice kullanarak komut arabelleğini GPU'ya gönderin. Kuyruk, tüm GPU komutlarını gerçekleştirerek yürütülmelerinin iyi bir şekilde sıralanmasını ve düzgün şekilde senkronize edilmesini sağlar. Kuyruğun submit() yöntemi, bu durumda yalnızca bir tane olsa da bir komut arabellekleri dizisi alır.

index.html

device.queue.submit([commandBuffer]);

Gönderdiğiniz komut arabelleği tekrar kullanılamaz. Bu nedenle, arabelleği saklamanıza gerek yoktur. Daha fazla komut göndermek istiyorsanız başka bir komut arabelleği oluşturmanız gerekir. Bu nedenle, bu iki adımın tek bir adımda birleştirilmesi oldukça yaygındır. Bu codelab'in örnek sayfalarında da bu şekilde yapılmıştır:

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 geri vermesine izin verin. Bu noktada tarayıcı, bağlamın mevcut dokusunu değiştirdiğinizi görür ve tuvali, bu dokuyu resim olarak gösterecek şekilde günceller. Bundan sonra tuval içeriklerini tekrar güncellemek isterseniz context.getCurrentTexture() işlevini tekrar çağırarak yeni bir doku almak için yeni bir komut arabelleği kaydedip göndermeniz gerekir.

  1. Sayfayı tekrar yükleyin. Tuvalin siyah renkle doldurulduğunu fark 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 bir tuval.

Bir renk seçin.

Ancak dürüst olmak gerekirse siyah kareler oldukça sıkıcı. Bu nedenle, bir sonraki bölüme geçmeden önce biraz zaman ayırarak bu bölümü kişiselleştirin.

  1. encoder.beginRenderPass() çağrısında, colorAttachment öğesine clearValue ile 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 hangi rengin kullanılacağını oluşturma geçişine bildirir. İçine iletilen sözlük dört değer içerir: kırmızı 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 olabilir ve birlikte bu renk kanalının değerini tanımlar. Örneğin:

  • { r: 1, g: 0, b: 0, a: 1 } parlak kırmızıdır.
  • { r: 1, g: 0, b: 1, a: 1 } parlak mordur.
  • { r: 0, g: 0.3, b: 0, a: 1 } koyu yeşildir.
  • { r: 0.5, g: 0.5, b: 0.5, a: 1 } orta gri renktedir.
  • { r: 0, g: 0, b: 0, a: 0 }, varsayılan saydam siyahtır.

Bu codelab'deki örnek kod ve ekran görüntüleri koyu mavi renkte olsa da istediğiniz rengi seçebilirsiniz.

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

Varsayılan temizleme renginin nasıl değiştirileceğini göstermek için koyu mavi renkte temizlenmiş bir tuval.

4. Geometri çizme

Bu bölümün sonunda, uygulamanız tuval üzerine basit bir geometri (renkli bir kare) çizecek. Bu kadar basit bir çıktı için çok fazla iş yapıyormuş gibi görüneceğini şimdiden belirtelim. Bunun nedeni, WebGPU'nun çok sayıda geometriyi çok verimli bir şekilde oluşturmak üzere tasarlanmış olmasıdır. Bu verimliliğin bir yan etkisi olarak, nispeten basit şeyleri yapmak alışılmadık derecede zor gelebilir. Ancak WebGPU gibi bir API'ye yöneliyorsanız biraz daha karmaşık bir şey yapmak istediğiniz için bu beklentiye girersiniz.

GPU'ların nasıl çizim yaptığını anlama

Daha fazla kod değişikliği yapmadan önce, GPU'ların ekranda gördüğünüz şekilleri nasıl oluşturduğuyla ilgili çok hızlı, basitleştirilmiş ve üst düzey bir genel bakış yapmanız faydalı olacaktır. (GPU oluşturmanın temel işleyiş şeklini biliyorsanız Köşeleri Tanımlama bölümüne geçebilirsiniz.)

Canvas 2D gibi, kullanıma hazır birçok şekil ve seçenek sunan bir API'nin aksine GPU'nuz yalnızca birkaç farklı şekil türüyle (veya WebGPU'nun kullandığı terimle ilkel) ilgilenir: noktalar, çizgiler ve üçgenler. Bu codelab'de yalnızca üçgenler kullanacaksınız.

Üçgenler, tahmin edilebilir ve verimli bir şekilde işlenmelerini sağlayan birçok güzel matematiksel özelliğe sahip oldukları için GPU'lar neredeyse yalnızca üçgenlerle çalışır. GPU ile çizdiğiniz hemen her şeyin, GPU tarafından çizilebilmesi için üçgenlere bölünmesi gerekir. Bu üçgenler de köşe noktalarıyla tanımlanmalıdır.

Bu noktalar veya köşeler, WebGPU ya da benzer API'ler tarafından tanımlanan bir Kartezyen koordinat sistemindeki bir noktayı tanımlayan X, Y ve (3D içerik için) Z değerleri açısından verilir. Koordinat sisteminin yapısını en kolay şekilde sayfanızdaki tuvalle ilişkisi açısından düşünebilirsiniz. Tuvaliniz ne kadar geniş veya uzun olursa olsun, sol kenar her zaman X ekseninde -1, sağ kenar ise her zaman X ekseninde +1 olur. Benzer şekilde, alt kenar Y ekseninde her zaman -1, üst kenar ise Y ekseninde her zaman +1 olur. Bu durumda (0, 0) her zaman tuvalin merkezi, (-1, -1) her zaman sol alt köşe ve (1, 1) her zaman sağ üst köşe olur. Bu, Kırpma Alanı olarak bilinir.

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

Köşeler başlangıçta bu koordinat sisteminde nadiren tanımlanır. Bu nedenle GPU'lar, köşeleri kırpma alanına dönüştürmek için gereken matematik işlemlerini ve köşeleri çizmek için gereken diğer hesaplamaları yapmak üzere köşe gölgelendiriciler adı verilen küçük programları kullanır. Örneğin, gölgelendirici bazı animasyonlar uygulayabilir veya köşeden ışık kaynağına olan yönü hesaplayabilir. Bu gölgelendiriciler, WebGPU geliştiricisi olarak sizin tarafınızdan yazılır ve GPU'nun çalışma şekli üzerinde inanılmaz bir kontrol sağlar.

GPU, bu dönüştürülmüş köşelerden oluşan tüm üçgenleri alır ve ekranda bunları çizmek için hangi piksellerin gerektiğini belirler. Ardından, her pikselin hangi renkte olması gerektiğini hesaplayan parça gölgelendirici adlı küçük bir program daha çalıştırır. Bu hesaplama, yeşil döndür kadar basit veya yüzeyin, yakındaki diğer yüzeylerden yansıyan, sisle filtrelenen ve yüzeyin ne kadar metalik olduğuna göre değiştirilen güneş ışığına göre açısını hesaplamak kadar karmaşık olabilir. Tamamen sizin kontrolünüzdedir. Bu durum hem güçlendirici hem de bunaltıcı olabilir.

Bu piksel renklerinin sonuçları daha sonra bir dokuda toplanır ve ekranda gösterilebilir.

Köşeleri tanımlama

Daha önce de belirtildiği gibi, Yaşam Oyunu simülasyonu hücrelerden oluşan bir ızgara olarak gösterilir. Uygulamanızda, etkin hücreleri etkin olmayan hücrelerden ayıran bir şekilde ızgarayı görselleştirme yöntemi olmalıdır. Bu codelab'de kullanılan yaklaşım, etkin hücrelere renkli kareler çizmek ve etkin olmayan hücreleri boş bırakmaktır.

Bu durumda, GPU'yu dört farklı noktayla (karenin dört köşesi için birer nokta) sağlamanız gerekir. Örneğin, tuvalin ortasına çizilen ve kenarlardan biraz ç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'e yerleştirmeniz gerekir. TypedArray'ler, bitişik bellek blokları 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 içinde dizideki her öğe tek bir işaretsiz bayttır. TypedArray'ler, WebAssembly, WebAudio ve (elbette) WebGPU gibi bellek düzenine duyarlı API'lerle veri göndermek ve almak için idealdir.

Kare örneğinde, değerler kesirli olduğundan Float32Array uygundur.

  1. Aşağıdaki dizi bildirimini kodunuza yerleştirerek diyagramdaki tüm köşe konumlarını tutan bir dizi oluşturun. Bu kodu, üst kısma yakın bir yere, context.configure() çağrısının hemen altına yerleştirebilirsiniz.

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şluk ve yorumun değerler üzerinde etkisi olmadığını, yalnızca kolaylık sağlamak ve daha okunabilir hale getirmek için kullanıldığını unutmayın. Her değer çiftinin bir köşe için X ve Y koordinatlarını oluşturduğunu görmenize yardımcı olur.

Ancak bir sorun var. GPU'lar üçgenler halinde çalışır, değil mi? Bu nedenle, köşeleri üçlü gruplar halinde sağlamanız gerekir. Dört kişilik bir grubunuz var. Çözüm, karenin ortasından geçen bir kenarı paylaşan iki üçgen oluşturmak için köşelerden ikisini tekrarlamaktır.

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

Şekildeki kareyi oluşturmak için (-0.8, -0.8) ve (0.8, 0.8) köşe noktalarını iki kez listelemeniz gerekir. Bir kez mavi üçgen, bir kez de kırmızı üçgen için. (Kareyi diğer iki köşeyle de bölebilirsiniz. Bu, sonucu değiştirmez.)

  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 netlik için iki üçgen arasında ayrım gösterilse de köşe konumları tam olarak aynıdır ve GPU bunları boşluksuz olarak oluşturur. Tek bir dolu kare olarak oluşturulur.

Köşe arabelleği oluşturma

GPU, JavaScript dizisindeki verilerle köşe çizemiyor. GPU'lar genellikle oluşturma için son derece optimize edilmiş kendi belleklerine sahiptir. Bu nedenle, GPU'nun çizim yaparken kullanmasını istediğiniz tüm verilerin bu belleğe yerleştirilmesi gerekir.

Köşe verileri de dahil olmak üzere birçok değer için GPU tarafındaki bellek GPUBuffer nesneleri aracılığıyla yönetilir. Arabellek, GPU'nun kolayca erişebildiği ve belirli amaçlar için işaretlenmiş bir bellek bloğudur. Bunu biraz GPU'nun görebildiği bir TypedArray olarak düşünebilirsiniz.

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

index.html

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

İlk olarak, arabelleğe bir etiket verdiğinizi fark edeceksiniz. Oluşturduğunuz her WebGPU nesnesine isteğe bağlı bir etiket verebilirsiniz. Bunu yapmanız kesinlikle önerilir. Etiket, nesnenin ne olduğunu belirlemenize yardımcı olduğu sürece istediğiniz dize olabilir. Herhangi bir sorunla karşılaşırsanız WebGPU'nun ürettiği hata mesajlarında bu etiketler kullanılır. Böylece, neyin yanlış gittiğini anlayabilirsiniz.

Ardından, arabelleğin boyutunu bayt cinsinden girin. 32 bitlik bir kayan sayının boyutunu ( 4 bayt) vertices dizinizdeki kayan sayıların sayısıyla (12) çarparak belirlediğiniz 48 baytlık bir arabellek gerekir. Neyse ki TypedArray'ler byteLength değerini sizin için hesaplar. Bu nedenle, arabellek oluştururken bu değeri kullanabilirsiniz.

Son olarak, arabelleğin kullanımını belirtmeniz gerekir. Bu, | ( bit düzeyinde VEYA) operatörüyle birleştirilmiş birden fazla işaretle birlikte bir veya daha fazla GPUBufferUsage işaretidir. Bu durumda, arabelleğin köşe verileri için kullanılmasını (GPUBufferUsage.VERTEX) ve verileri arabelleğe kopyalayabilmeyi (GPUBufferUsage.COPY_DST) istediğinizi belirtirsiniz.

Size döndürülen arabellek nesnesi opak olduğundan içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, çoğu özelliği sabittir. Bir GPUBuffer oluşturulduktan sonra yeniden boyutlandıramaz veya kullanım işaretlerini değiştiremezsiniz. Değiştirebileceğiniz şey, belleğindeki içeriklerdir.

Arabellek ilk oluşturulduğunda, içerdiği bellek sıfır olarak başlatılır. İçeriğini değiştirmenin çeşitli yolları vardır ancak en kolayı, kopyalamak istediğiniz bir TypedArray ile device.queue.writeBuffer() işlevini ç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

Artık içinde köşe verileri olan bir arabelleğiniz var ancak GPU açısından bu yalnızca bir bayt blobu. Bu araçla bir şeyler çizecekseniz biraz daha bilgi vermeniz gerekir. WebGPU'ya köşe verilerinin yapısı hakkında daha fazla bilgi vermeniz 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 nispeten kolayca parçalara ayrılabilir.

İlk verdiğiniz şey arrayStride olur. Bu, GPU'nun bir sonraki tepe noktasını ararken arabellekte ileriye doğru atlaması gereken bayt sayısıdır. Karenizin her köşesi, 32 bitlik iki kayan nokta sayısından oluşur. Daha önce de belirtildiği gibi, 32 bitlik bir kayan nokta sayısı 4 bayt olduğundan iki kayan nokta sayısı 8 bayttır.

Sırada dizi olan attributes özelliği var. Özellikler, her bir köşeye kodlanmış ayrı bilgi parçalarıdır. Köşeleriniz yalnızca bir özellik (köşe konumu) içerir ancak daha gelişmiş kullanım alanlarında genellikle köşelerin rengi veya geometrik yüzeyin yönü gibi birden fazla özellik içeren köşeler bulunur. Ancak bu, bu codelab'in kapsamı dışındadır.

Tek özelliğinizde önce verilerin format tanımlanır. Bu, GPU'nun anlayabileceği her bir köşe verisi türünü açıklayan GPUVertexFormat türlerinin listesinden gelir. Köşelerinizin her biri 32 bitlik iki kayan nokta içerdiğinden float32x2 biçimini kullanırsınız. Köşe verileriniz bunun yerine her biri dört 16 bitlik işaretsiz tam sayıdan oluşuyorsa örneğin uint16x4 kullanırsınız. Deseni görüyor musunuz?

Ardından, offset, bu özelliğin köşe noktasında kaç bayt sonra başladığını açıklar. Bu konuda endişelenmeniz gereken tek durum, arabelleğinizde birden fazla özellik olmasıdır. Bu durum, bu codelab sırasında ortaya çıkmaz.

Son olarak, shaderLocation'ı kullanabilirsiniz. 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 özellik, bir sonraki bölümde öğreneceğiniz köşe gölgelendirici içindeki belirli bir girişe bağlanır.

Bu değerleri şimdi tanımlamanıza rağmen henüz WebGPU API'ye aktarmadığınızı unutmayın. Bu değerler daha sonra kullanılacak olsa da bunları en kolay şekilde köşelerinizi tanımladığınız noktada düşünebilirsiniz.

Gölgelendiricilerle başlama

Artık oluşturmak istediğiniz verilere sahipsiniz ancak GPU'ya bu verileri tam olarak nasıl işleyeceğini söylemeniz gerekiyor. Bunun büyük bir kısmı gölgelendiricilerle gerçekleşir.

Gölgelendiriciler, 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: Köşe işleme, Parça işleme veya genel Hesaplama. GPU'da bulundukları için ortalama JavaScript'ten daha katı bir şekilde yapılandırılırlar. Ancak bu yapı, çok hızlı ve en önemlisi paralel olarak yürütmelerini sağlar.

WebGPU'daki gölgelendiriciler, WGSL (WebGPU Shading Language) adlı bir gölgelendirme dilinde yazılır. WGSL, söz dizimi açısından Rust'a biraz benzer. Ortak GPU işlerinin (ör. vektör ve matris matematiği) daha kolay ve hızlı yapılmasını amaçlayan özelliklere sahiptir. Gölgelendirme dilinin tamamını öğretmek bu codelab'in kapsamı dışındadır ancak umarız bazı basit örnekleri incelerken temel bilgileri edinebilirsiniz.

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

  • Aşağıdakileri vertexBufferLayout işaretinin altındaki kodunuza kopyalayarak gölgelendirici kodunuzu girebileceğiniz bir yer oluşturun:

index.html

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

Gölgelendiricileri oluşturmak için device.createShaderModule() işlevini çağırırsınız. Bu işlev için isteğe bağlı olarak label ve WGSL code dizesini sağlarsınız. (Çok satırlı dizelere izin vermek için burada ters tırnak kullandığınızı unutmayın.) Geçerli bir WGSL kodu eklediğinizde işlev, derlenmiş sonuçları içeren bir GPUShaderModule nesnesi döndürür.

Köşe gölgelendiricisini tanımlayın

GPU da aynı noktadan başladığı için köşe gölgelendiriciden başlayın.

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

Bu işlevlerin sırayla çağrılmayabileceğini anlamak önemlidir. Bunun yerine, GPU'lar bu tür gölgelendiricileri paralel olarak çalıştırmada mükemmeldir ve aynı anda yüzlerce (hatta binlerce!) köşe işleyebilir. Bu, GPU'ların inanılmaz hızının büyük bir bölümünü oluşturur ancak sınırlamaları vardır. Aşırı paralelleştirme sağlamak için köşe gölgelendiriciler birbirleriyle iletişim kuramaz. Her gölgelendirici çağrısı, tek seferde yalnızca tek bir köşe için verileri görebilir ve yalnızca tek bir köşe için değerler çıkışı yapabilir.

WGSL'de, köşe gölgelendirici işlevine istediğiniz adı verebilirsiniz ancak hangi gölgelendirici aşamasını temsil ettiğini belirtmek için önüne @vertex özelliği eklenmelidir. WGSL, işlevleri fn anahtar kelimesiyle belirtir, bağımsız değişkenleri bildirmek için parantezleri ve kapsamı tanımlamak için küme parantezlerini kullanır.

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

index.html (createShaderModule kodu)

@vertex
fn vertexMain() {

}

Ancak bir köşe gölgelendiricisi, klip alanında işlenen köşenin en azından son konumunu döndürmesi gerektiğinden bu geçerli değildir. Bu değer her zaman 4 boyutlu bir vektör olarak verilir. Vektörler, gölgelendiricilerde çok yaygın olarak kullanılan bir şeydir. Bu nedenle, dilde birinci sınıf temel öğeler olarak kabul edilir ve 4 boyutlu bir vektör için vec4f gibi kendi türleri vardır. 2D vektörler (vec2f) ve 3D vektörler (vec3f) için de benzer türler vardır.

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

index.html (createShaderModule kodu)

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

}

Elbette, işlevin bir dönüş türü varsa işlev gövdesinde gerçekten 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ğerleri, döndürülen değerde köşe noktasının kırpma alanında nerede bulunduğunu gösteren kayan nokta sayılardır.

  1. (0, 0, 0, 1) statik değerini döndürdüğünüzde, GPU ürettiği üçgenlerin tek bir noktadan ibaret olduğunu fark edip bunları sildiğinden hiçbir zaman bir şey görüntülemeyen geçerli bir köşe gölgelendiriciniz olur.

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 verileri kullanmak istersiniz. Bunu da @location() özelliği ve vertexBufferLayout içinde açıkladığınızla eşleşen bir türle işleviniz için bir bağımsız değişken tanımlayarak yaparsınız. shaderLocation değerini 0 olarak belirttiniz. Bu nedenle, WGSL kodunuzda bağımsız değişkeni @location(0) ile işaretleyin. Biçimi float32x2 olarak da tanımladınız. Bu, 2 boyutlu bir vektördür. Dolayısıyla WGSL'de bağımsız değişkeniniz vec2f olur. İstediğiniz adı verebilirsiniz ancak bunlar köşe konumlarınızı temsil ettiğinden pos gibi bir ad kullanmak mantıklı olacaktır.

  1. Gölgeleyici 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 de bu konuma geri dönmeniz gerekiyor. Konum 2 boyutlu bir vektör, dönüş türü ise 4 boyutlu bir vektör olduğundan bunu biraz değiştirmeniz gerekir. Yapmak istediğiniz şey, konum bağımsız değişkenindeki iki bileşeni alıp döndürülen vektörün ilk iki bileşenine yerleştirmek, son iki bileşeni ise 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);
}

Ancak bu tür eşlemeler gölgelendiricilerde çok yaygın olduğundan konum vektörünü uygun bir kısaltma ile ilk bağımsız değişken olarak da iletebilirsiniz. Bu, aynı anlama gelir.

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

index.html (createShaderModule kodu)

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

İlk köşe gölgelendiriciniz hazır! Bu yöntem çok basittir. Konum, neredeyse hiç değiştirilmeden iletilir ancak başlangıç için yeterlidir.

Parça gölgelendiriciyi tanımlama

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

Parça gölgelendiriciler her zaman köşe gölgelendiricilerden sonra çağrılır. GPU, köşe gölgelendiricilerin çıkışını alır ve üçgenleştirir. Böylece üç noktalı kümelerden üçgenler oluşturur. Ardından, çıkış rengi eklerinin hangi piksellerinin bu üç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 ve dokular gibi öğelerden kendisine gönderilen değerlerden hesaplanan bir renk döndürür. Bu renk, GPU tarafından renk ekine yazılır.

Parça gölgelendiriciler de tıpkı köşe gölgelendiriciler gibi büyük ölçüde paralel bir şekilde yürütülür. Giriş ve çıkışları açısından köşe gölgelendiricilerden biraz daha esnektirler ancak her üçgenin her pikseli için tek bir renk döndürdüklerini düşünebilirsiniz.

WGSL parça gölgelendirici işlevi, @fragment özelliğiyle gösterilir ve vec4f döndürür. Ancak bu durumda vektör, konumu değil rengi temsil eder. Dönüş değerine, beginRenderPass çağrısından hangi colorAttachment'ye döndürülen rengin yazıldığını belirtmek için @location özelliği verilmesi gerekir. Yalnızca bir ek 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. Bu nedenle vec4f(1, 0, 0, 1) parlak kırmızıdır ve bu, kareniz için uygun bir renk gibi görünmektedir. Ancak 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 da eksiksiz bir parça gölgelendiricidir. Bu, çok ilginç bir örnek değildir. Yalnızca her üçgenin her pikselini kırmızıya ayarlar ancak bu, şimdilik yeterlidir.

Özetlemek gerekirse, yukarıda ayrıntılı olarak açıklanan gölgelendirici kodunu ekledikten sonra createShaderModule çağrınız artık ş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);
    }
  `
});

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

Bir gölgelendirici modülü, tek başına oluşturma için kullanılamaz. Bunun yerine, device.createRenderPipeline() çağrılarak oluşturulan bir GPURenderPipeline parçası olarak kullanmanız gerekir. Oluşturma işlem hattı, hangi gölgelendiricilerin kullanılacağı, köşe arabelleklerindeki verilerin nasıl yorumlanacağı, hangi tür geometrinin oluşturulması gerektiği (çizgiler, noktalar, üçgenler vb.) gibi öğeler de dahil olmak üzere geometrinin nasıl çizileceğini kontrol eder.

Render işlem hattı, API'nin tamamındaki en karmaşık nesnedir ancak endişelenmeyin. Bu işleme iletebileceğ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.

  • Şunun 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 işlem hattının, işlem hattının ihtiyaç duyduğu giriş türlerini (köşe arabellekleri hariç) açıklayan bir layout'ye ihtiyacı vardır ancak sizde bu tür bir giriş yoktur. Neyse ki şimdilik "auto" değerini iletebilirsiniz. Bu durumda, işlem hattı kendi düzenini gölgelendiricilerden oluşturur.

Ardından, vertex aşamasıyla ilgili ayrıntıları sağlamanız gerekir. module, köşe gölgelendiricinizi içeren GPUShaderModule'dür ve entryPoint, her köşe çağrısı için çağrılan gölgelendirici kodundaki işlevin adını verir. (Tek bir gölgelendirici modülünde birden fazla @vertex ve @fragment işlevi olabilir.) Buffers, bu ardışık düzenle kullandığınız tepe noktası arabelleklerinde verilerinizin nasıl paketlendiğini açıklayan GPUVertexBufferLayout nesnelerinden oluşan bir dizidir. Neyse ki bunu vertexBufferLayout içinde daha önce tanımlamıştınız. Buradan geçebilirsiniz.

Son olarak, fragment aşamasıyla ilgili ayrıntılar yer alır. Bu, köşe aşaması gibi bir gölgelendirici modülü ve entryPoint'i de içerir. Son olarak, bu işlem hattının kullanıldığı targets tanımlanır. Bu, işlem hattının çıkışını yaptığı renk eklerinin doku gibi ayrıntılarını veren bir sözlük dizisidir. format Bu ayrıntıların, bu işlem hattının kullanıldığı tüm oluşturma geçişlerinin colorAttachments bölümünde verilen dokularla eşleşmesi gerekir. Render geçişiniz, tuval bağlamındaki dokuları kullanır ve biçimi için canvasFormat içinde kaydettiğiniz değeri kullandığından burada aynı biçimi iletirsiniz.

Bu, oluşturma işlem hattı oluştururken belirtebileceğiniz tüm seçeneklere yakın bile değildir ancak bu codelab'in ihtiyaçları için yeterlidir.

Kareyi çizin

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

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

index.html

// After encoder.beginRenderPass()

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

// before pass.end()

Bu, WebGPU'ya karenizi çizmek için gereken tüm bilgileri sağlar. Öncelikle, hangi işlem hattının çizim için kullanılacağını belirtmek üzere setPipeline() simgesini kullanırsınız. Kullanılan gölgelendiriciler, köşe verilerinin düzeni ve diğer ilgili durum verileri buna dahildir.

Ardından, karenizin köşelerini içeren arabellek ile setVertexBuffer() işlevini çağırırsınız. Bu arabellek, mevcut işlem hattının vertex.buffers tanımındaki 0. öğeye karşılık geldiği için 0 ile çağırırsınız.

Son olarak, tüm kurulum işlemlerinden sonra tuhaf bir şekilde basit görünen draw() araması yaparsınız. Tek yapmanız gereken, oluşturması gereken köşe sayısını iletmektir. Bu sayı, şu anda ayarlanmış köşe arabelleklerinden alınır ve şu anda ayarlanmış işlem hattıyla yorumlanır. Bunu 6 olarak sabit kodlayabilirsiniz ancak köşe dizisinden (12 kayan nokta / köşe başına 2 koordinat == 6 köşe) hesaplamak, kareyi örneğin bir daireyle değiştirmeye karar verirseniz elle güncellemeniz gereken daha az şey olacağı anlamına gelir.

  1. Ekranınızı yenileyin ve sıkı çalışmanızın sonucunu (nihayet) görün: büyük bir renkli kare.

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

5. Izgara çizme

Öncelikle kendinizi tebrik edin. Geometri verilerinin ilk kısımlarını ekrana getirmek, çoğu GPU API'sinde genellikle en zor adımlardan biridir. Buradan itibaren yapacağınız her şeyi daha küçük adımlara bölebilirsiniz. Böylece ilerlemenizi daha kolay doğrulayabilirsiniz.

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

  • Değişkenleri (üniforma olarak adlandırılır) JavaScript'ten gölgelendiriciye aktarma
  • Render davranışını değiştirmek için tek tip kullanma.
  • Aynı geometrinin birçok farklı varyantını çizmek için örnekleme nasıl kullanılır?

Izgarayı tanımlama

Bir ızgarayı oluşturmak için ızgarayla ilgili çok temel bir bilgiyi bilmeniz 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 daha kolaylaştırmak için ızgarayı kare (aynı genişlik ve yükseklik) olarak değerlendirin ve iki katı olan bir boyut kullanın. (Bu, daha sonra bazı matematik işlemlerini kolaylaştırır.) Sonunda daha büyük bir ızgara oluşturmak istiyorsunuz ancak bu bölümün geri kalanında ızgara boyutunuzu 4x4 olarak ayarlayın. Bu, bu bölümde kullanılan bazı matematiksel işlemleri göstermeyi kolaylaştırır. Daha sonra ölçeği artırın.

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

index.html

const GRID_SIZE = 4;

Ardından, tuval üzerine GRID_SIZE x GRID_SIZE kare sığdırabilmek için karelerinizi nasıl oluşturduğunuzu güncellemeniz gerekir. Bu nedenle, karenin çok daha küçük olması ve çok sayıda kare olması gerekir.

Bu soruna yaklaşabileceğiniz bir yöntem, köşe arabelleğinizi önemli ölçüde büyütmek ve içinde doğru boyut ve konumda GRID_SIZE kez GRID_SIZE kare tanımlamaktır. Bunun için kod yazmak aslında çok da zor olmaz. Yalnızca birkaç for döngüsü ve biraz matematik. Ancak bu yöntem, GPU'nun en iyi şekilde kullanılmasını sağlamaz ve efektin elde edilmesi için gerekenden daha fazla bellek kullanır. Bu bölümde, GPU'ya daha uygun bir yaklaşım ele alınmaktadır.

Tekdüzen arabellek oluşturma

Öncelikle, öğelerin nasıl görüntüleneceğini değiştirmek için kullanılan gölgelendiriciye seçtiğiniz ızgara boyutunu bildirmeniz gerekir. Boyutu doğrudan gölgelendiriciye kodlayabilirsiniz ancak bu durumda, ızgara boyutunu değiştirmek istediğiniz her seferde gölgelendiriciyi ve oluşturma işlem hattını yeniden oluşturmanız gerekir. Bu da maliyetli bir işlemdir. Daha iyi bir yöntem, ızgara boyutunu gölgelendiriciye uniforms olarak sağlamaktır.

Daha önce, köşe arabelleğindeki farklı bir değerin köşe gölgelendiricinin her çağrılmasına iletildiğini öğrenmiştiniz. Tekdüzen, her çağırmada aynı olan bir arabellek değeridir. Bir geometri parçası (ör. konumu), animasyonun tamamı (ör. geçerli saat) veya uygulamanın tüm kullanım ömrü (ör. kullanıcı tercihi) için ortak olan değerleri iletmek amacıyla kullanılı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 tamamen aynı olduğundan çok tanıdık gelecektir. Bunun nedeni, tek tip değişkenlerin WebGPU API'ye, köşe noktalarıyla aynı GPUBuffer nesneleri aracılığıyla iletilmesidir. Aradaki temel fark, bu kez usage öğesinin GPUBufferUsage.VERTEX yerine GPUBufferUsage.UNIFORM içermesidir.

Bir gölgelendiricide tek tip değişkenlere erişme

  • Aşağıdaki kodu ekleyerek bir forma 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, tekdüzen arabelleğe kopyaladığınız diziyle eşleşen 2 boyutlu bir kayan nokta vektörüdür. Ayrıca üniformanın @group(0) ve @binding(0) konumlarında bağlı olduğunu 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, köşe konumunu ızgara vektörüne bölersiniz. pos 2 boyutlu bir vektör ve grid 2 boyutlu bir vektör olduğundan WGSL, bileşen bazında bölme işlemi gerçekleştirir. Başka bir deyişle, sonuç vec2f(pos.x / grid.x, pos.y / grid.y) demeyle aynıdır.

Bu tür vektör işlemleri, birçok oluşturma ve hesaplama tekniği bunlara dayandığından GPU gölgelendiricilerinde çok yaygındır.

Bu durumda (4 birimlik bir ızgara boyutu kullandıysanız) oluşturduğunuz karenin orijinal boyutunun dörtte biri olacağı anlamına gelir. Dört tanesini bir satıra veya sütuna sığdırmak istiyorsanız bu boyut idealdir.

Bağlama grubu oluşturma

Ancak, gölgelendiricideki tekdüzen değişkeni bildirmek, onu oluşturduğunuz arabelleğe bağlamaz. Bunun için bağlama grubu oluşturup ayarlamanız gerekir.

Bağlama grubu, gölgelendiricinize aynı anda erişilebilir kılmak istediğiniz kaynakların bir koleksiyonudur. Bu, tek tip arabellek gibi çeşitli arabellek türlerini ve burada ele alınmayan ancak WebGPU oluşturma tekniklerinin yaygın parçaları olan dokular ve örnekleyiciler gibi diğer kaynakları içerebilir.

  • Aşağıdaki kodu, tekdüzen arabellek ve oluşturma ardışık düzeni oluşturulduktan sonra ekleyerek tekdüzen arabelleğinizle 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'a ek olarak, bu bağlama grubunun hangi kaynak türlerini içerdiğini açıklayan bir layout de gerekir. Bu, gelecekteki bir adımda daha ayrıntılı olarak inceleyeceğiniz bir konudur ancak şu an için layout: "auto" ile işlem hattını oluşturduğunuzdan işlem hattınızdan bağlama grubu düzenini isteyebilirsiniz. Bu, işlem hattının, bağlayıcı kodun kendisinde bildirdiğiniz bağlamalardan otomatik olarak bağlama grubu düzenleri oluşturmasına neden olur. Bu durumda, getBindGroupLayout(0) yazarak 0 değerini, gölgelendiricide yazdığınız @group(0) değerine karşılık gelecek şekilde ayarlarsınız.

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

  • binding, gölgelendiricide girdiğiniz @binding() değeriyle eşleşir. Bu durumda, 0.
  • Belirtilen bağlama dizinindeki değişkene göstermek istediğiniz gerçek kaynak olan resource. Bu durumda, tek tip arabelleğiniz.

İşlev, opak ve değişmez bir tutma yeri olan GPUBindGroup döndürür. Bağlama grubu oluşturulduktan sonra, bu grubun işaret ettiği kaynakları 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ı bu değişikliği yansıtır.

Bağlama grubunu bağlama

Bağlama grubu oluşturulduktan sonra, çizim yaparken WebGPU'ya bu grubu kullanmasını söylemeniz gerekir. Neyse ki bu işlem oldukça basittir.

  1. Render geçişine geri dönün ve draw() yönteminden önce bu yeni satırı 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) değerine karşılık gelir. @group(0) içinde yer alan her @binding, bu bağlama grubundaki kaynakları kullanıyor.

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

  1. Sayfanızı yenilediğinizde aşağıdakine benzer bir ekranla karşılaşırsınız:

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

Yaşasın! Kare artık önceki boyutunun dörtte biri kadar. Bu çok fazla olmasa da tekdüzeliğinizin gerçekten uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.

Gölgelendiricide geometriyi değiştirme

Artık gölgelendiricide ızgara boyutuna başvurabildiğinize göre, oluşturduğunuz geometriyi istediğiniz ızgara desenine uyacak şekilde değiştirmek için çalışmaya başlayabilirsiniz. Bunu yapmak için tam olarak neyi başarmak istediğinizi düşünün.

Tuvalinizi kavramsal olarak ayrı hücrelere bölmeniz gerekir. X ekseninin sağa doğru gidildikçe, Y ekseninin ise yukarı doğru gidildikçe arttığı kuralını korumak için ilk hücrenin tuvalin sol alt köşesinde olduğunu varsayalım. Bu işlem, mevcut kare geometrinizin ortada olduğu aşağıdaki gibi bir düzen oluşturur:

Her hücrenin, merkezinde şu anda oluşturulan kare geometrisiyle görselleştirilmesi sırasında Normalleştirilmiş Cihaz Koordinatı alanının bölüneceği kavramsal ızgaranın bir resmi.

Buradaki zorluk, hücre koordinatları verildiğinde kare geometrisini bu hücrelerin herhangi birine yerleştirmenizi sağlayan bir yöntemi gölgelendiricide bulmaktır.

Öncelikle, tuvalin merkezini çevreleyecek şekilde tanımlandığı için karenizin hücrelerle düzgün şekilde hizalanmadığını görebilirsiniz. Kareyi, hücrelerin içinde düzgün bir şekilde hizalanacak şekilde yarım hücre kaydırmak isteyebilirsiniz.

Bu sorunu düzeltmenin bir yolu, karenin köşe arabelleğini güncellemektir. Köşeleri, sol alt köşesi örneğin (-0.8, -0.8) yerine (0.1, 0.1) olacak şekilde kaydırarak bu kareyi hücre sınırlarıyla daha iyi hizalayabilirsiniz. Ancak, köşe noktalarının gölgelendiricinizde nasıl işleneceği üzerinde tam kontrole sahip olduğunuz için gölgelendirici kodunu kullanarak bunları yerine yerleştirmek de aynı derecede kolaydır.

  1. Köşe gölgelendirici modülünü aşağıdaki kodla 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, her köşeyi ızgara boyutuyla bölmeden önce bir birim yukarı ve sağa taşır (bir birimin, kırpma alanının yarısı olduğunu unutmayın). Sonuç, başlangıç noktasının hemen dışında, ızgarayla düzgün şekilde hizalanmış bir karedir.

Tuvalin, (2, 2) hücresinde kırmızı bir kare bulunan 4x4 ızgaraya kavramsal olarak bölünmüş hâlinin görselleştirilmesi

Ardından, tuvalinizin koordinat sistemi (0, 0) konumunu merkeze ve (-1, -1) konumunu sol alta yerleştirdiğinden ve (0, 0) konumunun sol altta olmasını istediğinizden, geometrinizin konumunu bu köşeye taşımak için ızgara boyutuna böldükten sonra (-1, -1) ile çevirmeniz gerekir.

  1. Geometrinizin konumunu aşağıdaki gibi ç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);
}

Artık kareniz (0, 0) hücresinde düzgün bir şekilde konumlandırılmış durumda.

Tuvalin, (0, 0) hücresinde kırmızı bir kare bulunan 4x4 ızgaraya kavramsal olarak bölünmüş hâlinin görselleştirilmesi

Farklı bir hücreye yerleştirmek isterseniz ne yapmanız gerekir? Bunu, gölgelendiricinizde bir cell vektörü bildirerek ve bunu let cell = vec2f(1, 1) gibi statik bir değerle doldurarak belirleyin.

Bunu gridPos öğesine eklerseniz algoritmadaki - 1 işlemi geri alınır. Bu nedenle, bunu yapmamanız gerekir. Bunun yerine, kareyi her hücre için yalnızca bir ızgara birimi (tuvalin dörtte biri) kadar taşımak istiyorsunuz. grid ile bir kez daha bölmeniz gerekiyor.

  1. Izgara konumlandırmanızı 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, 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 ile kavramsal olarak 4x4 ızgaraya bölünmüş hâlinin görselleştirilmesi

Hm. İstediğiniz gibi değil mi?

Bunun nedeni, tuval koordinatları -1 ile +1 arasında değiştiği için gerçekte 2 birim genişliğinde olmasıdır. Yani bir tepe noktasını tuvalin dörtte biri kadar taşımak istiyorsanız 0, 5 birim taşımanız gerekir. GPU koordinatlarıyla akıl yürütürken bu hatayı yapmak kolaydır. Neyse ki bu sorunu düzeltmek de aynı derecede kolay.

  1. Telafinizi 2 ile çarpın:

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

Bu da size tam olarak istediğinizi verir.

Tuvalin, (1, 1) hücresinde kırmızı bir kare bulunan 4x4 ızgaraya kavramsal olarak bölünmüş hâlinin görselleştirilmesi

Ekran görüntüsü şu şekilde görünür:

Koyu mavi arka plan üzerinde kırmızı bir karenin ekran görüntüsü. Kırmızı kare, önceki şemada açıklandığı gibi aynı konumda çizilir ancak ızgara yerleşimi olmadan.

Ayrıca, artık cell değerini ızgara sınırları içindeki herhangi bir değere ayarlayabilir ve ardından kareyi istediğiniz konumda oluşturulmuş olarak görmek için yenileyebilirsiniz.

Çizim örnekleri

Kareyi biraz matematik yardımıyla istediğiniz yere yerleştirebildiğinize göre, bir sonraki adımda ızgaranın her hücresinde bir kare oluşturmanız gerekir.

Bu soruna yaklaşmanın bir yolu, hücre koordinatlarını tek tip bir arabelleğe yazmak, ardından ızgaradaki her kare için draw işlevini bir kez çağırmak ve tek tipi her seferinde güncellemek olabilir. Ancak GPU'nun her seferinde yeni koordinatın JavaScript tarafından yazılmasını beklemesi gerektiğinden bu işlem çok yavaş olur. GPU'dan iyi performans elde etmenin anahtarlarından biri, sistemin diğer kısımlarını beklerken harcadığı süreyi en aza indirmektir.

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

  1. GPU'ya, ızgarayı dolduracak kadar kare örneği istediğinizi söylemek 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öşesini 16 (GRID_SIZE * GRID_SIZE) kez çizmesini istediğinizi söyler. Ancak sayfayı yenilediğinizde aşağıdaki mesajı görmeye devam ediyorsanız:

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

Neden? Bunun nedeni, 16 karenin tamamını aynı noktaya çizmenizdir. Gölgelendiricide, geometriyi örnek bazında yeniden konumlandıran bazı ek mantıklar olması gerekir.

Shader'da, köşe arabelleğinizden gelen pos gibi köşe özelliklerinin yanı sıra WGSL'nin yerleşik değerleri olarak bilinenlere de erişebilirsiniz. Bunlar WebGPU tarafından hesaplanan değerlerdir ve bu değerlerden biri instance_index'dır. instance_index, gölgelendirici mantığınızın bir parçası olarak kullanabileceğiniz, 0 ile number of instances - 1 arasındaki işaretsiz 32 bitlik bir sayıdır. Değeri, aynı örneğin parçası olan her işlenmiş köşe için aynıdır. Bu durumda, köşe arabelleğinizdeki her konum için bir kez olmak üzere, köşe gölgelendiriciniz instance_index değeri 0 olan altı kez çağrılır. Ardından instance_index 1 ile altı kez daha, sonra instance_index 2 ile altı kez daha ve bu şekilde devam eder.

Bunu uygulamada görmek için instance_index yerleşik işlevini gölgelendirici girişlerinize eklemeniz gerekir. Bu işlemi konumla 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şkene istediğiniz adı verin. (Örnek kodla eşleşmesi için instance olarak adlandırabilirsiniz.) Ardından, bunu gölgelendirici mantığının bir parçası olarak kullanın.

  1. Hücre koordinatları 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 yenilerseniz gerçekten birden fazla kare olduğunu görürsünüz. Ancak 16 öğenin tamamını göremezsiniz.

Koyu mavi bir arka plan üzerinde sol alt köşeden sağ üst köşeye doğru çapraz bir çizgi oluşturan dört kırmızı kare.

Bunun nedeni, oluşturduğunuz hücre koordinatlarının (0, 0), (1, 1), (2, 2)... (15, 15) şeklinde olması ancak bunlardan yalnızca ilk dördünün tuval üzerine sığmasıdır. İstediğiniz ızgarayı oluşturmak için instance_index öğesini, her dizinin ızgaranızdaki benzersiz bir hücreyle eşleneceği şekilde dönüştürmeniz gerekir. Örneğin:

Tuvalin, her hücresi doğrusal bir örnek dizinine de karşılık gelecek şekilde kavramsal olarak 4x4&#39;lük bir ızgaraya bölünmüş hâlinin görselleştirilmesi.

Bunun hesabı oldukça basittir. Her hücrenin X değeri için instance_index ve ızgara genişliğinin modülünü istiyorsunuz. Bunu % operatörüyle WGSL'de yapabilirsiniz. Ayrıca, her hücrenin Y değeri için instance_index değerini ızgara genişliğine bölmek ve kesirli kalanları atmak istersiniz. Bu işlemi WGSL'nin floor() işleviyle yapabilirsiniz.

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

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

Kodu güncelledikten sonra nihayet uzun zamandır beklediğiniz kareler ızgarasına sahip olursunuz.

Koyu mavi arka plan üzerinde dört satır ve dört sütun halinde kırmızı kareler.

  1. Şimdi çalışıyor olduğuna göre geri dönüp ızgara boyutunu artırabilirsiniz.

index.html

const GRID_SIZE = 32;

Koyu mavi arka plan üzerinde 32 satır ve 32 sütun kırmızı kare.

Tada! Bu kılavuzu artık çok büyük hale getirebilirsiniz ve ortalama bir GPU bunu sorunsuz bir şekilde işleyebilir. GPU performansıyla ilgili darboğazlarla karşılaşmadan çok önce ayrı kareleri görmeyi bırakırsınız.

6. Ekstra kredi: Daha renkli hale getirin.

Bu noktada, codelab'in geri kalanı için temel bilgileri edindiğinizden bir sonraki bölüme kolayca geçebilirsiniz. Ancak aynı rengi paylaşan karelerden oluşan ızgara işe yarasa da pek heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla işleri biraz daha aydınlatabilirsiniz.

Shader'larda yapıları kullanma

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

Verileri köşe gölgelendiriciden geçirmenin tek yolu döndürmektir. Bir köşe gölgelendiricinin her zaman bir konum döndürmesi gerekir. Bu nedenle, konumla birlikte başka veriler de döndürmek istiyorsanız bu verileri bir yapının içine yerleştirmeniz gerekir. WGSL'deki yapılar, bir veya daha fazla adlandırılmış özellik içeren adlandırılmış nesne türleridir. Mülkler, @builtin ve @location gibi özelliklerle de işaretlenebilir. Bunları işlevlerin dışında tanımlarsınız ve gerektiğinde işlevlere iletip işlevlerden çıkarabilirsiniz. Ö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);
}
  • Aynı şeyi, işlev girişi ve çıkışı için yapılar kullanarak 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 giriş konumuna ve örnek dizinine input ile başvurmanız gerektiğini ve önce döndürdüğünüz yapının değişken olarak bildirilmesi ve bağımsız özelliklerinin ayarlanması gerektiğini unutmayın. Bu durumda, çok fazla fark yaratmaz ve hatta gölgelendirici işlevini biraz daha uzun hale getirir. Ancak gölgelendiricileriniz daha karmaşık hale geldikçe yapıları kullanmak, verilerinizi düzenlemenize yardımcı olabilecek harika bir yol olabilir.

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

@fragment işlevinin olabildiğince 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ıyor ve çıkış olarak düz bir renk (kırmızı) veriyorsunuz. Ancak gölgelendirici, renklendirdiği geometri hakkında daha fazla bilgiye sahip olsaydı bu ek verileri kullanarak işleri biraz daha ilginç hale getirebilirdiniz. Örneğin, her karenin rengini hücre koordinatına göre değiştirmek istiyorsanız ne yapmanız gerekir? @vertex aşaması, hangi hücrenin oluşturulduğunu bilir. Bunu @fragment aşamasına iletmeniz yeterlidir.

Köşe ve parça aşamaları arasında herhangi bir veriyi iletmek için bu veriyi, bizim seçtiğimiz bir @location ile çıkış yapısında eklemeniz gerekir. Hücre koordinatını iletmek istediğiniz için bunu daha önce oluşturduğunuz VertexOutput yapısına ekleyin ve döndürmeden önce @vertex işlevinde ayarlayın.

  1. Köşe gölgelendiricinizin dönüş değerini aşağıdaki gibi 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 ile bir bağımsız değişken ekleyerek değeri alın. (Adların eşleşmesi gerekmez ancak eşleşirse 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 yapı da 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. Kodunuzda bu işlevlerin her ikisi de aynı gölgelendirici modülünde tanımlandığından, @vertex aşamasının çıkış yapısını yeniden kullanmak da bir alternatiftir. Adlar ve konumlar doğal olarak tutarlı olduğundan bu yöntem, 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 deseni seçerseniz seçin, sonuç olarak @fragment işlevinde hücre numarasına erişebilir ve rengi etkilemek için bu numarayı kullanabilirsiniz. Yukarıdaki kodlardan herhangi biriyle çıkış şu şekilde görünür:

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

Şimdi kesinlikle daha fazla renk var ancak görünümü pek hoş değil. Yalnızca sol ve alt satırların neden farklı olduğunu merak edebilirsiniz. Bunun nedeni, @fragment işlevinden döndürdüğünüz renk değerlerinin her kanalın 0 ile 1 aralığında olmasını beklemesi ve bu aralığın dışındaki değerlerin bu aralığa sabitlenmesidir. Hücre değerleriniz ise her eksende 0 ile 32 arasında değişir. Burada gördüğünüz gibi, ilk satır ve sütun kırmızı veya yeşil renk kanalında hemen 1 değerine ulaşıyor ve bundan sonraki her hücre aynı değere sabitleniyor.

Renkler arasında daha yumuşak bir geçiş istiyorsanız her renk kanalı için kesirli bir değer döndürmeniz gerekir. İdeal olarak her eksende sıfırla başlayıp birle biten bu değerler, grid ile bir kez daha bölme işlemi yapmanız gerektiği anlamına gelir.

  1. Parça gölgelendiricisini ş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, ızgaranın tamamında çok daha güzel bir renk geçişi sağladığını görebilirsiniz.

Farklı köşelerde siyahtan kırmızıya, yeşile ve sarıya geçiş yapan karelerden oluşan bir ızgara.

Bu kesinlikle bir iyileştirme olsa da artık sol altta, ızgaranın siyahlaştığı talihsiz bir karanlık köşe var. Hayat Oyunu simülasyonunu yapmaya başladığınızda, ızgaranın zor görünen bir bölümü olanları gizler. It would be nice to brighten that up.

Neyse ki kullanabileceğiniz, kullanılmamış bir renk kanalı (mavi) var. İdeal olarak, diğer renklerin en koyu olduğu yerlerde mavinin en parlak olmasını ve diğer renklerin yoğunluğu arttıkça mavinin solmasını istersiniz. Bunu yapmanın en kolay yolu, kanalın 1'den başlamasını sağlamak ve hücre değerlerinden birini çıkarmaktır. c.x veya c.y olabilir. İkisini de deneyip tercih ettiğinizi seçin.

  1. Parça gölgelendiriciye daha parlak renkler ekleyin (ör. aşağıdaki gibi):

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 güzel görünüyor.

Farklı köşelerde kırmızıdan yeşile, maviden sarıya geçiş yapan karelerden oluşan bir ızgara.

Bu kritik bir adım değildir. Ancak daha iyi göründüğü için ilgili kontrol noktası kaynak dosyasına dahil edildi ve bu codelab'deki ekran görüntülerinin geri kalanında bu daha renkli ızgara yansıtıldı.

7. Hücre durumunu yönetme

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

Tek ihtiyacınız her hücre için bir açma/kapama sinyalidir. Bu nedenle, neredeyse her değer türünden oluşan büyük bir dizi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun, tek tip arabelleklerin bir başka kullanım alanı olduğunu düşünebilirsiniz. Bu işi yapabilirsiniz ancak tek tip arabelleklerin boyutu sınırlı olduğundan, dinamik olarak boyutlandırılmış dizileri desteklemediğinden (dizi boyutunu gölgelendiricide belirtmeniz gerekir) ve hesaplama gölgelendiricileri tarafından yazılamadığından bu daha zordur. Son öğe, Yaşam Oyunu simülasyonunu bir hesaplama gölgelendiricisinde GPU'da yapmak istediğiniz için en sorunlu olanıdır.

Neyse ki tüm bu sınırlamaları ortadan kaldıran başka bir arabellek seçeneği var.

Depolama arabelleği oluşturma

Depolama arabellekleri, işlem gölgelendiricilerinde okunup yazılabilen ve tepe gölgelendiricilerinde okunabilen genel amaçlı arabelleklerdir. Çok büyük olabilirler ve bir gölgelendiricide belirli bir boyut bildirilmesi gerekmez. Bu da onları genel belleğe çok daha fazla benzetir. Hücre durumunu depolamak için bunu kullanırsınız.

  1. Hücre durumunuz için bir depolama arabelleği oluşturmak üzere, muhtemelen artık tanıdık görünmeye başlayan bir arabellek oluşturma kodu snippet'i 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 ve tekdüze arabelleklerinizde olduğu gibi, device.createBuffer() işlevini uygun boyutta çağırın ve bu kez GPUBufferUsage.STORAGE kullanımını belirttiğinizden emin olun.

Aynı boyuttaki TypedArray'i değerlerle doldurup device.queue.writeBuffer() işlevini çağırarak arabelleği eskisi gibi doldurabilirsiniz. Arabelleğinizin ızgara üzerindeki etkisini görmek istediğiniz için arabelleği tahmin edilebilir bir şeyle doldurarak başlayın.

  1. Aşağıdaki kodu kullanarak her üçüncü hücreyi 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ölgelendiricide 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 üniformaların eklenme şekline çok benzer.

  1. Gölgelendiricinizi 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!

Öncelikle, bağlama noktasını ekleyin. Bu nokta, ızgara üniformasının hemen altına yerleştirilir. @group ile grid forması aynı olsun istiyorsunuz ancak @binding numarası farklı olmalı. Farklı arabellek türünü yansıtmak için var türü storage'dir ve cellState için verdiğiniz tür, JavaScript'teki Uint32Array ile eşleşmesi için tek bir vektör yerine u32 değerlerinden oluşan bir dizidir.

Ardından, @vertex işlevinizin 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 kullanabilirsiniz.

Eyalet, hücrenin etkin olmadığını söylüyorsa 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 ile ölçeklendirme geometride değişiklik yapmazken 0 ile ölçeklendirme geometrinin tek bir noktaya daralmasına neden olur. Bu nokta daha sonra GPU tarafından atılır.

  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 f32 olarak yayınlanmalı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 etkisini görebilmek için depolama arabelleğini bir bağlama grubuna ekleyin. Tekdüzen arabellek ile aynı @group'nın parçası olduğundan, JavaScript kodunda da aynı bağlama grubuna ekleyin.

  • Depolama tamponunu 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şin binding değerinin, gölgelendiricideki ilgili değerin @binding() değeriyle eşleştiğinden emin olun.

Bu işlemden sonra yenileyebilir ve kalıbın kılavuzda görünmesini sağlayabilirsiniz.

Koyu mavi arka plan üzerinde sol alttan sağ üste doğru giden renkli karelerden oluşan çapraz çizgiler.

Ping-pong arabellek düzenini kullanma

Oluşturduğunuz gibi çoğu simülasyon genellikle durumunun en az iki kopyasını kullanır. Simülasyonun her adımında, durumun bir kopyasından okuma yapıp diğerine yazma işlemi gerçekleştirirler. Ardından, bir sonraki adımda kağıdı çevirin ve daha önce yazdıkları durumdan okumaya devam edin. Bu durum, durumun en güncel sürümü her adımda durum kopyaları arasında ileri geri hareket ettiğinden genellikle ping pong (masa tenisi) modeli olarak adlandırılır.

Bu neden gerekli? Basitleştirilmiş bir örneğe bakalım: Her adımda etkin blokları bir hücre sağa taşıdığınız çok basit bir simülasyon yazdığınızı düşünün. Kolay anlaşılması 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 bir adımda dizinin sonuna kadar gider. Neden? Çünkü durumu yerinde güncellemeye devam ediyorsunuz. Bu nedenle, etkin hücreyi sağa taşıyıp sonraki hücreye bakıyorsunuz ve... hey! Etkinleştirildi. Doğru yere tekrar taşıyın. Verileri gözlemlerken aynı anda değiştirmeniz sonuçları bozar.

Ping pong modelini kullanarak simülasyonun bir sonraki adımını her zaman yalnızca son adımın sonuçlarını kullanarak gerçekleştirebilirsiniz.

// 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. İki özdeş arabellek oluşturmak için depolama arabelleği ayırmanızı güncelleyerek bu kalıbı kendi kodunuzda 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 arabellek arasındaki farkı görselleştirmek için arabellekleri 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. Render'ınızda farklı depolama arabelleklerini göstermek için bağlama gruplarınızı iki farklı varyanta sahip olacak ş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] }
    }],
  })
];

Render döngüsü oluşturma

Şimdiye kadar sayfa yenileme başına yalnızca bir çekim yaptınız ancak artık 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.

Render döngüsü, içeriğinizi belirli aralıklarla tuvale çizen, sonsuza kadar tekrarlanan bir döngüdür. Sorunsuz animasyonlar kullanmak isteyen birçok oyun ve diğer içerikler, geri çağırmaları ekranın yenilenme hızıyla aynı oranda (saniyede 60 kez) planlamak için requestAnimationFrame() işlevini kullanır.

Bu uygulama da bunu kullanabilir ancak bu durumda, simülasyonun ne yaptığını daha kolay takip edebilmek için güncellemelerin daha uzun adımlarla yapılmasını isteyebilirsiniz. Simülasyonunuzun güncellenme hızını kontrol edebilmek için döngüyü kendiniz yönetin.

  1. Öncelikle simülasyonumuzun güncelleneceği bir hız seçin (200 ms iyi bir hızdır ancak isterseniz daha yavaş veya daha hızlı da gidebilirsiniz). Ardından, tamamlanan simülasyon adımlarının sayısı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çin kullandığınız tüm kodu yeni bir işleve taşıyın. Bu işlevi setInterval() ile istediğiniz aralıkta tekrarlanacak şekilde planlayın. İşlevin adım sayısını da güncellediğinden emin olun ve hangi bağlama grubunun bağlanacağını seçmek için bu bilgiyi kullanın.

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

Uygulamayı çalıştırdığınızda tuvalin, oluşturduğunuz iki durum arabelleğini göstererek ileri geri döndüğünü görürsünüz.

Koyu mavi arka plan üzerinde sol alttan sağ üste doğru giden renkli karelerden oluşan çapraz çizgiler. Koyu mavi arka plan üzerinde rengarenk karelerden oluşan dikey çizgiler.

Böylece, oluşturma tarafındaki işlemlerinizi tamamlamış olursunuz. Bir sonraki adımda, nihayet hesaplama gölgelendiricilerini kullanmaya başlayacağınız Yaşam Oyunu simülasyonunun çıktısını göstermeye hazırsınız.

WebGPU'nun oluşturma özelliklerinin, burada incelediğiniz küçük bölümden çok daha fazlasını içerdiği açıktır ancak geri kalan kısım bu kod laboratuvarının kapsamı dışındadır. Bununla birlikte, WebGPU'nun oluşturma işleminin nasıl çalıştığına dair yeterli bir fikir vereceğini ve 3D oluşturma gibi daha gelişmiş teknikleri anlamayı kolaylaştıracağını umuyoruz.

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

Şimdi de bulmacanın son büyük parçasına geçelim: Hayat Oyunu simülasyonunu bir hesaplama gölgelendiricisinde gerçekleştirme.

Sonunda hesaplama gölgelendiricilerini kullanabilirsiniz!

Bu codelab boyunca işlem gölgelendiricileri hakkında soyut bilgiler edindiniz. Peki bunlar tam olarak nedir?

İşlem gölgelendiriciler, GPU'da son derece paralel çalışacak şekilde tasarlanmaları bakımından köşe ve parça gölgelendiricilere benzer. Ancak diğer iki gölgelendirici aşamasının aksine, belirli bir giriş ve çıkış kümesi yoktur. Verileri yalnızca seçtiğiniz kaynaklardan (ör. depolama arabellekleri) okuyup yazarsınız. Bu, her bir köşe, örnek veya piksel için bir kez yürütmek yerine gölgelendirici işlevinin kaç kez çağrılmasını istediğinizi belirtmeniz gerektiği anlamına gelir. Ardından, gölgelendiriciyi çalıştırdığınızda hangi çağırmanın işlendiği size bildirilir ve buradan hangi verilere erişeceğinize ve hangi işlemleri yapacağınıza karar verebilirsiniz.

Compute shader'lar, vertex ve fragment shader'lar gibi bir shader modülünde oluşturulmalıdır. Bu nedenle, başlamak için bunu kodunuza ekleyin. Uyguladığınız diğer gölgelendiricilerin yapısı göz önüne alındığında, hesaplama gölgelendiricinizin ana işlevinin @compute özelliğiyle işaretlenmesi gerektiğini tahmin edebilirsiniz.

  1. Aşağıdaki kodu kullanarak bir hesaplama gölgelendiricisi 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() {

    }`
});

GPU'lar 3D grafikler için sıkça kullanıldığından işlem gölgelendiricileri, gölgelendiricinin X, Y ve Z eksenleri boyunca belirli sayıda çağrılmasını isteyebileceğiniz şekilde yapılandırılır. Bu sayede, 2D veya 3D ızgaraya uygun işleri çok kolay bir şekilde gönderebilirsiniz. Bu da kullanım alanınız için harika bir özelliktir. Bu gölgelendiriciyi, simülasyonunuzun her hücresi için bir kez olmak üzere GRID_SIZE kez çağırmak istiyorsunuz.GRID_SIZE

GPU donanım mimarisinin yapısı nedeniyle bu ızgara çalışma gruplarına ayrılır. Çalışma gruplarının X, Y ve Z boyutları vardır. Bu boyutlar 1 olabilir ancak çalışma gruplarınızı biraz daha büyük hale getirmenin genellikle performans açısından faydaları vardır. Gölgelendiriciniz için 8 x 8 gibi biraz rastgele bir iş grubu boyutu seçin. Bu, JavaScript kodunuzda takip etmek için kullanışlıdır.

  1. Çalışma grubu boyutunuz için şu şekilde bir sabit tanımlayın:

index.html

const WORKGROUP_SIZE = 8;

Ayrıca, az önce tanımladığınız sabiti kolayca kullanabilmek için JavaScript'in şablon değişmezlerini kullanarak çalışma grubu boyutunu gölgelendirici işlevine de eklemeniz gerekir.

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

index.html (Compute createShaderModule call)

@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) gruplar halinde yapıldığını bildirir. (Belirtmediğiniz tüm eksenler varsayılan olarak 1 olur ancak en azından X eksenini belirtmeniz gerekir.)

Diğer gölgelendirici aşamalarında olduğu gibi, hangi çağırmada olduğunuzu anlamak ve hangi işi yapmanız gerektiğine karar vermek için hesaplama gölgelendirici işlevinize giriş olarak kabul edebileceğiniz çeşitli @builtin değerleri vardır.

  1. Şu şekilde bir @builtin değeri ekleyin:

index.html (Compute createShaderModule call)

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

}

global_invocation_id yerleşik işlevini iletirsiniz. Bu işlev, gölgelendirici çağırma işlemlerinin ızgarasında nerede olduğunuzu gösteren, işaretsiz tam sayılardan oluşan üç boyutlu bir vektördür. 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)... (31, 31, 0)'e kadar olan sayıları elde edersiniz. Bu da bu sayıları üzerinde işlem yapacağınız hücre dizini olarak değerlendirebileceğiniz anlamına gelir.

İşlem gölgelendiriciler, köşe ve parça gölgelendiricilerde olduğu gibi tek tip değişkenler de kullanabilir.

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

index.html (Compute createShaderModule call)

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

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

}

Köşe gölgelendiricisinde olduğu gibi, hücre durumunu da depolama arabelleği olarak kullanıma sunarsınız. Ancak bu durumda ikisine de ihtiyacınız var. Compute shader'ların köşe konumu veya parça rengi gibi zorunlu bir çıkışı olmadığından, sonuçları compute shader'dan almanın tek yolu değerleri bir depolama arabelleğine veya dokuya yazmaktır. Daha önce öğrendiğiniz ping-pong yöntemini kullanın. Izgaranın mevcut durumunu besleyen bir depolama arabelleğiniz ve ızgaranın yeni durumunu yazdığınız bir arabelleğiniz vardır.

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

index.html (Compute createShaderModule call)

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

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

}

İlk depolama arabelleğinin var<storage> ile tanımlandığını ve bu nedenle salt okunur olduğunu, ikinci depolama arabelleğinin ise var<storage, read_write> ile tanımlandığını unutmayın. Bu sayede, arabelleği okuyup yazabilir ve arabelleği hesaplama gölgelendiricinizin çıkışı olarak kullanabilirsiniz. (WebGPU'da yalnızca yazma depolama modu yoktur.)

Ardından, hücre dizininizi doğrusal depolama dizisine eşlemenin bir yolunu bulmanız gerekir. Bu, temel olarak köşe gölgelendiricide yaptığınız işlemin tam tersidir. Köşe gölgelendiricide doğrusal instance_index değerini alıp 2D ızgara hücresine eşlemiştiniz. (Bu işlem için algoritmanızın vec2f(i % grid.x, floor(i / grid.x)) olduğunu hatırlatırız.)

  1. Diğer yöne gidecek 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 call)

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

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

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

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

Son olarak, algoritmanın çalıştığını görmek için çok basit bir algoritma uygulayın: Hücre şu anda açıksa kapanır, kapalıysa açılır. Bu örnek henüz Hayat Oyunu değil ancak hesaplama gölgelendiricisinin çalıştığını göstermek için yeterli.

  1. Basit algoritmayı aşağıdaki gibi ekleyin:

index.html (Compute createShaderModule call)

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

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

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

Şimdilik hesaplama gölgelendiricinizle ilgili bilgiler bu kadar. Ancak sonuçları görebilmeniz için yapmanız gereken birkaç değişiklik daha var.

Bind Group ve Pipeline Layouts'u kullanma

Yukarıdaki gölgelendiriciden fark edebileceğiniz bir nokta, bunun büyük ölçüde oluşturma işlem hattınızla aynı girişleri (tek tip ve depolama arabellekleri) kullanmasıdır. Bu nedenle, aynı bağlama gruplarını kullanıp işi bitirebileceğinizi düşünebilirsiniz, değil mi? İyi haberimiz var: Bunu yapabilirsiniz. Bunu yapabilmek için biraz daha fazla manuel kurulum yapmanız gerekir.

Herhangi bir bağlama grubu oluşturduğunuzda GPUBindGroupLayout sağlamanız gerekir. Daha önce, oluşturma işlem hattında getBindGroupLayout() çağrısı yaparak bu düzeni elde ediyordunuz. Bu da oluşturma işlem hattını oluştururken layout: "auto" sağladığınız için düzeni otomatik olarak oluşturuyordu. Bu yaklaşım yalnızca tek bir işlem hattı kullandığınızda iyi sonuç verir. Ancak kaynakları paylaşmak isteyen birden fazla işlem hattınız varsa düzeni açıkça oluşturmanız ve ardından hem bağlama grubuna hem de işlem hatlarına sağlamanız gerekir.

Bunun nedenini anlamak için şu örneği inceleyin: Oluşturma işlem hatlarınızda tek bir tekdüzen arabellek ve tek bir depolama arabelleği kullanıyorsunuz ancak yeni yazdığınız hesaplama gölgelendiricisinde ikinci bir depolama arabelleğine ihtiyacınız var. İki gölgelendirici, tek tip ve ilk depolama arabelleği için aynı @binding değerlerini kullandığından bunları işlem hatları arasında paylaşabilirsiniz. Oluşturma işlem hattı ise kullanmadığı ikinci depolama arabelleğini yoksayar. Yalnızca belirli bir işlem hattı tarafından kullanılan kaynakları 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() işlevini ç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 tanımladığınız için bağlama grubunu oluşturmaya benzer bir yapıya sahiptir. 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, kaynağın binding numarasını verirsiniz. Bu numara (bağlama grubunu oluştururken öğrendiğiniz gibi) gölgelendiricilerdeki @binding değeriyle eşleşir. Ayrıca, kaynağı hangi gölgelendirici aşamalarının kullanabileceğini belirten GPUShaderStage işaretleri olan visibility değerini de sağlarsınız. Hem tekdüzen hem de ilk depolama arabelleğinin tepe noktası ve hesaplama gölgelendiricilerinde erişilebilir olmasını istiyorsunuz ancak ikinci depolama arabelleğinin yalnızca hesaplama gölgelendiricilerinde erişilebilir olması gerekiyor.

Son olarak, kullanılan kaynak türünü belirtirsiniz. Bu, neyi göstermeniz gerektiğine bağlı olarak farklı bir sözlük anahtarıdır. Burada üç kaynağın tamamı arabellek olduğundan her birinin seçeneklerini tanımlamak için buffer tuşunu kullanırsınız. texture veya sampler gibi başka seçenekler de vardır ancak burada bunlara ihtiyacınız yoktur.

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

bindGroupLayout oluşturulduktan sonra, bağlama gruplarınızı oluştururken bağlama grubunu işlem hattından sorgulamak yerine bindGroupLayout değerini iletebilirsiniz. Bu durumda, az önce tanımladığınız düzene uymak için her bağlama grubuna yeni bir depolama arabelleği girişi eklemeniz gerekir.

  1. Bağlama grubu oluşturma işlemini aşağıdaki gibi 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ğinden oluşturma işlem hattını da aynı şeyi kullanacak şekilde güncellemeniz gerekir.

  1. GPUPipelineLayout oluşturun.

index.html

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

Bir işlem hattı düzeni, bir veya daha fazla işlem hattının kullandığı bağlama grubu düzenlerinin bir listesidir (bu durumda bir tane vardır). Dizideki bağlama grubu düzenlerinin sırası, gölgelendiricilerdeki @group özellikleriyle eşleşmelidir. (Bu, bindGroupLayout hesabının @group(0) ile ilişkilendirildiği anlamına gelir.)

  1. İşlem hattı düzenini aldıktan sonra, oluşturma işlem hattını "auto" yerine bu düzeni kullanacak şekilde 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
    }]
  }
});

İşlem ardışık düzenini oluşturma

Köşe ve parça gölgelendiricilerinizi kullanmak için oluşturma işlem hattına ihtiyacınız olduğu gibi, hesaplama gölgelendiricinizi kullanmak için de hesaplama işlem hattına ihtiyacınız vardır. Neyse ki işlem ardışık düzenleri, oluşturma ardışık düzenlerine kıyasla çok daha az karmaşıktır. Çünkü ayarlanacak herhangi bir durumu yoktur, yalnızca gölgelendirici ve düzen vardır.

  • Aşağıdaki kodu kullanarak bir işlem 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 işlem hattında olduğu gibi, "auto" yerine yeni pipelineLayout öğesini ilettiğinizi unutmayın. Bu, hem oluşturma işlem hattınızın hem de hesaplama işlem hattınızın aynı bağlama gruplarını kullanabilmesini sağlar.

Compute geçişleri

Bu noktada işlem hattını gerçekten kullanmaya başlayabilirsiniz. Render işleminizi bir render geçişinde yaptığınızı göz önünde bulundurarak, hesaplama işlemini bir hesaplama geçişinde yapmanız gerektiğini tahmin edebilirsiniz. Hem hesaplama hem de oluşturma işlemleri aynı komut kodlayıcıda gerçekleşebilir. Bu nedenle, updateGrid işlevinizi biraz karıştırmanız gerekir.

  1. Kodlayıcı oluşturma işlemini işlevin en üstüne taşıyın ve ardından step++'dan önce kodlayıcıyla bir hesaplama geçişi 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...

İşlem geçişleri, işlem işlem hatları gibi eklerle ilgili endişelenmenize gerek olmadığından oluşturma karşılıklarına kıyasla çok daha kolay başlatılır.

Hesaplama geçişinin sonuçları, oluşturma geçişinde hemen kullanılabildiğinden hesaplama geçişini oluşturma geçişinden önce yapmak istersiniz. Bu nedenle, geçişler arasında step sayısını artırarak hesaplama işlem hattının çıkış arabelleğini oluşturma işlem hattının giriş arabelleği haline getirirsiniz.

  1. Ardından, işlem geçişi içinde işlem hattını ayarlayın ve bağlama grubunu bağlayın. Bağlama grupları arasında geçiş yapmak için oluşturma geçişinde 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şinde olduğu gibi çizim yapmak yerine, çalışmayı hesaplama 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 bir nokta, dispatchWorkgroups() öğesine ilettiğiniz sayının, çağırma sayısı olmamasıdır. Bunun yerine, gölgelendiricinizdeki @workgroup_size tarafından tanımlanan, yürütülecek iş grubu sayısıdır.

Shader'ın tüm ızgaranızı kaplamak için 32x32 kez yürütülmesini istiyorsanız ve iş grubu boyutunuz 8x8 ise 4x4 iş grubu göndermeniz gerekir (4 * 8 = 32). Bu nedenle, ızgara boyutunu çalışma grubu boyutuna bölüp bu değeri dispatchWorkgroups()'ya aktarırsınız.

Şimdi sayfayı tekrar yenileyebilirsiniz. Izgaranın her güncellemede tersine döndüğünü görürsünüz.

Koyu mavi arka plan üzerinde sol alttan sağ üste doğru giden renkli karelerden oluşan çapraz çizgiler. Koyu mavi arka plan üzerinde, sol alttan sağ üste doğru iki kare genişliğinde renkli karelerden oluşan çapraz çizgiler. Önceki görüntünün ters çevrilmiş hali.

Hayat Oyunu algoritmasını uygulama

Nihai algoritmayı uygulamak için bilgi işlem gölgelendiricisini güncellemeden önce, depolama arabelleği içeriğini başlatan koda geri dönüp her sayfa yüklemesinde rastgele bir arabellek oluşturacak şekilde güncellemek istersiniz. (Düzenli desenler, Yaşam Oyunu için çok ilginç başlangıç noktaları oluşturmaz.) Değerleri istediğiniz gibi rastgele hale getirebilirsiniz ancak makul sonuçlar veren kolay bir başlangıç yöntemi vardır.

  1. Her hücreye rastgele bir durumda başlamak için cellStateArray başlatma 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 nihayet Game of Life simülasyonunun mantığını uygulayabilirsiniz. Buraya gelmek için gereken her şeyden sonra gölgelendirici kodu hayal kırıklığı yaratacak kadar basit olabilir.

Öncelikle, herhangi bir hücrenin kaç komşusunun etkin olduğunu bilmeniz gerekir. Hangilerinin etkin olduğuyla değil, yalnızca sayısıyla ilgileniyorsunuz.

  1. Komşu hücre verilerini almayı kolaylaştırmak için, belirli koordinatın cellStateIn değerini döndüren bir cellActive işlevi ekleyin.

index.html (Compute createShaderModule call)

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

cellActive işlevi, hücre etkinse 1 değerini döndürür. Bu nedenle, çevreleyen sekiz hücre için cellActive işlevini çağırmanın dönüş değerini eklediğinizde kaç komşu hücrenin etkin olduğunu görürsünüz.

  1. Etkin komşu sayısını şu şekilde bulabilirsiniz:

index.html (Compute createShaderModule call)

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

Ancak bu durum küçük bir soruna yol açar: Kontrol ettiğiniz hücre tahtanın kenarında değilse ne olur? Şu anki cellIndex() mantığınıza göre, ya sonraki veya önceki satıra taşar ya da arabelleğin kenarından çıkar.

Hayat Oyunu'nda bu sorunu çözmenin yaygın ve kolay bir yolu, ızgaranın kenarındaki hücrelerin, ızgaranın karşı kenarındaki hücreleri komşuları olarak kabul etmesini sağlamaktır. Bu şekilde, bir tür sarma efekti oluşturulur.

  1. cellIndex() işlevinde küçük bir değişiklik yaparak ızgara sarmalama desteği eklendi.

index.html (Compute createShaderModule call)

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

Izgara boyutunu aşan X ve Y hücrelerini sarmak için % operatörünü kullanarak depolama arabelleği sınırlarının dışına hiçbir zaman erişmemeyi sağlayabilirsiniz. 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 etkinliğini kaybeder.
  • İki veya üç komşusu olan etkin hücreler etkin kalır.
  • Tam olarak üç komşusu olan tüm etkin olmayan hücreler etkin hale gelir.
  • Üçten fazla komşusu olan hücreler etkinliğini kaybeder.

Bunu bir dizi if ifadesiyle yapabilirsiniz ancak WGSL, bu mantık için uygun olan switch ifadelerini de destekler.

  1. Game of Life mantığını aşağıdaki gibi uygulayın:

index.html (Compute createShaderModule call)

let i = cellIndex(cell.xy);

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

Referans olarak, nihai hesaplama gölgelendirici modülü çağrısı artık ş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;
        }
      }
    }
  `
});

İşlem tamamlandı. Hepsi bu kadar! Sayfanızı yenileyin ve yeni oluşturduğunuz hücresel otomatın büyümesini izleyin.

Hayat Oyunu simülasyonundaki örnek bir durumun ekran görüntüsü. Koyu mavi arka plan üzerinde renkli hücreler gösteriliyor.

9. Tebrikler!

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

Yapabilecekleriniz

Daha fazla bilgi

Referans belgeleri