İlk WebGPU uygulamanız

İlk WebGPU uygulamanız

Bu codelab hakkında

subjectSon güncelleme Nis 15, 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 yeni ve modern bir API'dir.

Modern API

WebGPU'dan önce, WebGPU özelliklerinin bir alt kümesini sunan WebGL vardı. Bu teknoloji, yeni bir sınıf zengin web içeriği oluşturma olanağı sağladı ve geliştiriciler bu teknolojiyle harika şeyler oluşturdu. Ancak 2007'de yayınlanan OpenGL ES 2.0 API'sini temel alıyordu. Bu API ise daha da eski bir OpenGL API'sini temel alıyordu. GPU'lar bu süre zarfında önemli ölçüde gelişti ve onlarla 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 sunduğu avantajları web platformuna getirir. GPU özelliklerini platformlar arası bir şekilde etkinleştirmeye odaklanırken web'de doğal hissettiren ve temel alındığı 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şturmayla ilişkilendirilir. WebGPU de buna istisna 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 GPU'nuzun genel amaçlı, yüksek paralellikteki iş yüklerini gerçekleştirme potansiyelini de 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ş bir parçası olarak kullanılabilir.

Bugünkü 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.

Oluşturacağınız uygulama

Bu codelab'de, WebGPU'yu kullanarak Conway's Game of Life'i (Conway'in Yaşam Oyunu) 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üne ait ekran görüntüsü

Yaşam Oyunu, bir hücre ızgarasındaki hücrelerin belirli kurallara göre zaman içinde durumunu değiştirdiği hücresel otomat olarak bilinen bir oyundur. Yaşam Oyunu'nda hücreler, komşu hücrelerinin kaç tanesinin etkin olduğuna bağlı olarak etkin veya devre dışı hale gelir. Bu da izlerken dalgalanmalar gösteren ilginç desenlere yol açar.

Neler öğreneceksiniz?

  • WebGPU'yu ayarlama ve tuval yapılandırması.
  • Basit 2D geometri çizme.
  • Ç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ölgelendiricileri kullanma.

Bu codelab, WebGPU'nun temel kavramlarını tanıtmaya odaklanır. Bu makale, API'nin kapsamlı bir incelemesi olarak tasarlanmamıştır ve 3D matris matematik gibi sıklıkla ilişkili konularla ilgili bilgileri kapsamaz (veya gerektirmez).

Gerekenler

  • ChromeOS, macOS veya Windows'ta Chrome'un son sürümlerinden biri (113 veya sonraki sürümler) WebGPU, tarayıcı 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 olmanız gerekir.

WebGL, Metal, Vulkan veya Direct3D gibi diğer grafik API'lerini bilmeniz zorunludur. Ancak bu API'lerle ilgili deneyiminiz varsa WebGPU ile birçok benzerlik olduğunu fark edeceksiniz. Bu benzerlikler, öğrenme sürecinize hızlı bir başlangıç yapmanıza yardımcı olabilir.

2. Hazırlanın

Kodu alma

Bu kod laboratuvarının 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 kod yazmanıza gerek yoktur. Ancak https://glitch.com/edit/#!/your-first-webgpu-app adresinde kontrol noktası olarak kullanabileceğiniz bazı çalışan örnekler mevcuttur. Bu örneklere göz atabilir ve takıldığınızda referans olarak kullanabilirsiniz.

Geliştirici Konsolu'nu kullanın.

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

WebGPU ile geliştirirken, özellikle de yeni başlayan biriyseniz sorunlarla karşılaşacaksınız. Bu normaldir. API'nin arkasındaki geliştiriciler, GPU geliştirmeyle çalışmanın zorluklarının farkındadır ve WebGPU kodunuz bir hataya neden olduğunda geliştirici konsolunda sorunu tespit etmenize ve düzeltmenize yardımcı olacak çok ayrıntılı ve faydalı mesajlar alabilmeniz için çok çalıştı.

Herhangi bir web uygulaması üzerinde ç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şla

Tek amacınız hesaplama yapmaksa WebGPU'yu ekranda hiçbir şey göstermeden kullanabilirsiniz. Ancak kod laboratuvarımızda yapacağımız gibi bir şey oluşturmak istiyorsanız bir kanvas kullanmanız gerekir. Bu nedenle, başlangıç için iyi bir yer.

İçinde tek bir <canvas> öğesi ve kanvas öğesini sorguladığımız bir <script> etiketi bulunan yeni bir HTML dokümanı oluşturun. (Alternatif olarak Glitch'teki 00-starter-page.html dosyasını da kullanabilirsiniz.)

  • 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'ye geçebilirsiniz. Öncelikle, WebGPU gibi API'lerin tüm web ekosistemine yayılması biraz zaman alabileceğini göz önünde bulundurmalısınız. Bu nedenle, ilk önlem olarak kullanıcının tarayıcısının WebGPU'yu kullanıp kullanamayacağını kontrol etmek iyi bir fikirdir.

  1. WebGPU için giriş noktası görevi gören navigator.gpu nesnesinin olup olmadığını kontrol etmek üzere aşağıdaki kodu ekleyin:

index.html

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

İdeal olarak, sayfayı WebGPU'yu kullanmayan bir moda geçirerek WebGPU'nun kullanılamadığını kullanıcıya bildirmek istersiniz. (Bunun yerine WebGL kullanılabilir mi?) Ancak bu codelab'in amacı doğrultusunda, kodun daha fazla yürütülmesini durdurmak için bir hata atmanız yeterlidir.

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

  1. Bağdaştırıcı almak için navigator.gpu.requestAdapter() yöntemini kullanın. Bir promise döndürür. Bu nedenle, await ile çağırmak en uygun yöntemdir.

index.html

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

Uygun bir adaptör 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 ortaya çıkabilir.

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

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

  1. adapter.requestDevice() çağrısını yaparak cihazı alın. Bu çağrı da bir promise döndürür.

index.html

const device = await adapter.requestDevice();

requestAdapter()'te 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 burada iletilen seçenekler vardır ancak amaçlarınız için varsayılanlar gayet iyi çalışır.

Canvas'u yapılandırma

Cihazınız hazır. Sayfada bir şey göstermek için cihazı kullanmak istiyorsanız yapmanız gereken bir şey daha var: Tuvali, yeni oluşturduğunuz cihazla kullanılacak şekilde yapılandırın.

  • Bunu yapmak için önce canvas.getContext("webgpu")'ı çağırarak kanvastan 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 kullanacağınız çağrıyla aynıdır.) Ardından, döndürülen 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'tır.

WebGPU'nun resim verilerini depolamak için kullandığı nesneler dokulardır. 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 kod laboratuvarının kapsamı dışındadır. Önemli olan, kanvas bağlamının, kodunuzun çizebileceği dokular sağladığı ve kullandığınız biçimin, kanvasın bu resimleri ne kadar verimli bir şekilde gösterdiğini etkileyebileceğ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ının yapılmasına neden olabilirsiniz.

Neyse ki WebGPU, tuvaliniz için hangi biçimi kullanacağınızı size söyler. Bu nedenle, bu konulardan endişelenmenize gerek yoktur. Neredeyse her durumda, yukarıda gösterildiği gibi navigator.gpu.getPreferredCanvasFormat() çağrılarak döndürülen değeri iletmek istersiniz.

Tuvali temizleme

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

Bunu yapmak (veya WebGPU'da hemen hemen her şeyi yapmak) için GPU'ya ne yapacağını bildiren bazı komutlar göndermeniz gerekir.

  1. Bunu yapmak için cihazın, GPU komutlarını kaydetmek için bir arayüz sağlayan bir 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) ilgilidir. Bu nedenle, bir oluşturma geçişi başlatmak için encoder komutunu kullanmanız gerekir.

Oluşturma geçişleri, WebGPU'daki tüm çizim işlemlerinin gerçekleştiği yerlerdir. Her biri, gerçekleştirilen tüm çizim komutlarının çıktısını alan dokuları tanımlayan bir beginRenderPass() çağrısıyla başlar. Daha gelişmiş kullanımlar, oluşturulan geometrinin derinliğini depolamak veya kenar yumuşatma sağlamak gibi çeşitli amaçlara sahip ek olarak adlandırılan çeşitli dokular sağlayabilir. Ancak bu uygulama için yalnızca bir tane gereklidir.

  1. context.getCurrentTexture() işlevini çağırarak daha önce oluşturduğunuz tuval bağlamından doku alın. Bu işlev, tuvalin width ve height özelliklerine ve context.configure() işlevini çağırdığınız sırada belirtilen format değerine uygun piksel genişliği ve yüksekliğine sahip bir doku döndürür.

index.html

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

Doku, colorAttachment öğesinin view mülkü olarak verilir. Oluşturma geçişleri, dokudaki hangi kısımların oluşturulacağını belirten bir GPUTexture yerine bir GPUTextureView sağlamanız gerekir. Bu durum yalnızca daha gelişmiş kullanım alanları için önemlidir. Bu nedenle, burada createView() işlevini doku üzerinde hiçbir bağımsız değişken olmadan çağırırsınız. Bu, oluşturma geçişinin dokudaki tüm alanı kullanmasını istediğinizi gösterir.

Ayrıca, oluşturma geçişinin başladığında ve sona erdiğinde doku üzerinde 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 çizimlerin sonuçlarının dokuya kaydedilmesini istediğinizi belirtir.

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

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

index.html

pass.end();

Bu çağrıların yapılmasının GPU'nun herhangi bir işlem yapmasına neden olmayacağını bilmek önemlidir. GPU'nun daha sonra yapması için komutları kaydediyorlar.

  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. GPUDevice'un queue özelliğini kullanarak komut arabelleğini GPU'ya gönderin. Sıra, tüm GPU komutlarını yürüterek bunların iyi bir şekilde sıralandığından ve düzgün bir şekilde senkronize edildiğinden emin olur. Sıranın submit() yöntemi bir komut arabelleği dizisi alır ancak bu durumda yalnızca bir tane vardır.

index.html

device.queue.submit([commandBuffer]);

Gönderdiğiniz bir komut arabelleği tekrar kullanılamaz. Bu nedenle, bu arabelleği saklamaya 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ıma birleştirildiğini görmek oldukça yaygındır (bu kod laboratuvarının örnek sayfalarında olduğu gibi):

index.html

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

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

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

WebGPU&#39;nun, kanvas içeriğini temizlemek için başarıyla kullanıldığını gösteren siyah bir kanvas.

Bir renk seçin.

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

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

index.html

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

clearValue, oluşturma geçişine, geçişin başında clear işlemini gerçekleştirirken hangi rengi kullanması gerektiğini bildirir. Bu işleve 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 bir değer 0 ile 1 arasında olabilir ve birlikte söz konusu 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 ton gridir.
  • { r: 0, g: 0, b: 0, a: 0 }, varsayılan olarak saydam siyahtır.

Bu kod laboratuvarındaki örnek kod ve ekran görüntülerinde koyu mavi kullanılmıştır ancak 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 renge ayarlanmış bir kanvas.

4. Geometri çizme

Bu bölümün sonunda uygulamanız tuvale basit bir geometri çizecek: renkli bir kare. Bu kadar basit bir çıktı için çok fazla iş gibi göründüğünü belirtmek isteriz. Bunun nedeni, WebGPU'nun çok sayıda geometriyi çok verimli bir şekilde oluşturmak için tasarlanmış olmasıdır. Bu verimliliğin bir yan etkisi, nispeten basit şeyler yapmanın alışılmadık derecede zor görünmesidir. Ancak WebGPU gibi bir API'ye yöneliyorsanız biraz daha karmaşık bir şey yapmak istediğiniz için bu beklenti de vardır.

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ğuna dair çok hızlı, basitleştirilmiş ve üst düzey bir genel bakış yapmanız faydalı olacaktır. (GPU oluşturmanın işleyişiyle ilgili temel bilgilere aşinaysanız Köşe Noktalarını Tanımlama bölümüne atlayabilirsiniz.)

Kullanabileceğiniz çok sayıda şekil ve seçeneğe sahip Canvas 2D gibi bir API'nin aksine, GPU'nuz yalnızca birkaç farklı şekil türüyle (veya WebGPU tarafından adlandırıldığı şekliyle ilkel) çalışır: noktalar, çizgiler ve üçgenler. Bu codelab'de yalnızca üçgenler kullanacaksınız.

GPU'lar neredeyse yalnızca üçgenlerle çalışır. Bunun nedeni, üçgenlerin tahmin edilebilir ve verimli bir şekilde işlenmesini kolaylaştıran birçok güzel matematiksel özelliğe sahip olmasıdır. GPU ile çizdiğiniz neredeyse her şeyin, GPU'nun çizebilmesi için üçgene bölünmesi ve bu üçgenlerin köşe noktalarıyla tanımlanması gerekir.

Bu noktalar veya köşeler, WebGPU veya benzer API'ler tarafından tanımlanan bir kartezyen koordinat sisteminde bir noktayı tanımlayan X, Y ve (3D içerik için) Z değerleri olarak verilir. Koordinat sisteminin yapısını, sayfanızdaki tuvalle ilişkisi açısından düşünmek en kolayıdır. Kanvasınızın genişliği veya yüksekliği ne olursa olsun sol kenar X ekseninde her zaman -1, sağ kenar ise X ekseninde her zaman +1 olur. Benzer şekilde, alt kenar Y ekseninde her zaman -1, üst kenar ise Y ekseninde +1 olur. Yani (0, 0) her zaman kanvasın ortasında, (-1, -1) her zaman sol alt köşede ve (1, 1) her zaman sağ üst köşededir. Buna Klip Alanı denir.

Normalleştirilmiş cihaz koordinatı alanını görselleştiren basit bir grafik.

Köşe noktaları başlangıçta bu koordinat sisteminde nadiren tanımlanır. Bu nedenle GPU'lar, köşe noktalarını klip alanına dönüştürmek için gereken tüm matematik işlemlerini ve köşe noktalarını çizmek için gereken diğer tüm hesaplamaları yapmak üzere köşe noktası gölgelendiricileri adlı küçük programlardan yararlanı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 siz tarafından yazılır ve GPU'nun işleyiş şekli üzerinde inanılmaz miktarda kontrol sağlar.

Ardından GPU, bu dönüştürülmüş köşelerden oluşan tüm üçgenleri alır ve bunları çizmek için ekrandaki hangi piksellere ihtiyaç duyulduğunu belirler. Ardından, her pikselin ne renk olması gerektiğini hesaplayan, yazdığınız parça gölgelendirici adlı başka bir küçük program çalıştırılır. Bu hesaplama, yeşil döndür kadar basit veya yüzeyin, yakındaki diğer yüzeylerden gelen güneş ışığına göre açısını hesaplama, sisle filtreleme ve yüzeyin ne kadar metalik olduğuna göre değiştirme kadar karmaşık olabilir. Tamamen sizin kontrolünüz altında olan bu işlem hem güçlendirici hem de bunaltıcı olabilir.

Bu piksel renklerinin sonuçları daha sonra bir dokuya 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ırarak ızgaranın görselleştirilmesini sağlayan bir yöntem olmalıdır. Bu kod laboratuvarının yaklaşımı, etkin hücrelere renkli kareler çizip etkin olmayan hücreleri boş bırakmaktır.

Bu, GPU'ya karenin dört köşesinin her biri için birer farklı nokta sağlamanız gerektiği anlamına gelir. Örneğin, kanvasın ortasına çizilen ve kenarlardan biraz içeri ç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 beslemek için değerleri bir TypedArray içine yerleştirmeniz gerekir. Henüz aşina değilseniz TypedArrays, bitişik bellek blokları ayırmanıza ve serideki her öğeyi belirli bir veri türü olarak yorumlamanıza olanak tanıyan bir JavaScript nesne grubudur. Örneğin, bir Uint8Array içinde dizideki her öğe tek bir imzasız bayttır. TypedArray'lar, WebAssembly, WebAudio ve (elbette) WebGPU gibi bellek düzenine duyarlı API'lerle veri alışverişinde bulunmak için mükemmeldir.

Kare örneğinde, değerler kesirli olduğu için Float32Array uygundur.

  1. Aşağıdaki dizi tanımını kodunuza yerleştirerek diyagramdaki tüm köşe konumlarını içeren bir dizi oluşturun. Bu düğmeyi en üstte, context.configure() çağrısının hemen altında 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 yorumların değerler üzerinde hiçbir etkisi olmadığını unutmayın. Bunlar yalnızca size kolaylık sağlamak ve daha okunaklı hale getirmek içindir. Her değer çiftinin bir köşenin X ve Y koordinatlarını oluşturduğunu görmenize yardımcı olur.

Ancak bir sorun var. GPU'lar üçgenlerle çalışır. Yani köşeleri üçlü gruplar halinde sağlamanız gerekir. Dört kişilik bir grubunuz var. Çözüm, karenin ortasından 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.

Şemadan kare oluşturmak için (-0,8; -0,8) ve (0,8; 0,8) köşe noktalarını mavi üçgen için bir kez, kırmızı üçgen için bir kez olmak üzere iki kez listelemeniz gerekir. (Köşeleri diğer iki köşeyle de bölmeyi seçebilirsiniz. Bu işlemde bir fark yoktur.)

  1. Önceki vertices dizinini şu ş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 bir ayrım gösterilmesine rağmen köşe konumları tamamen aynıdır ve GPU bunları boşluk olmadan oluşturur. Tek bir katı kare olarak oluşturulur.

Köşe ara belleği oluşturma

GPU, JavaScript dizisinden alınan verilerle köşe noktaları çizemez. GPU'ların genellikle oluşturma için yüksek oranda optimize edilmiş kendi bellekleri vardır. Bu nedenle, GPU'nun çizim yaparken kullanmasını istediğiniz tüm verilerin bu belleğe yerleştirilmesi gerekir.

Nokta verileri 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 GPU'da görünen bir TypedArray olarak düşünebilirsiniz.

  1. Köşe noktalarınızı tutacak bir arabellek oluşturmak için vertices dizinizin tanımından sonra device.createBuffer() işlevine 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 dikkat etmeniz gereken nokta, arabelleğe bir etiket vermenizdir. Oluşturduğunuz her WebGPU nesnesine isteğe bağlı bir etiket verilebilir ve bunu kesinlikle yapmanız gerekir. Etiket, nesnenin ne olduğunu belirlemenize yardımcı olduğu sürece istediğiniz bir dize olabilir. Herhangi bir sorunla karşılaşırsanız bu etiketler, WebGPU'nun oluşturduğu hata mesajlarında neyin yanlış gittiğini anlamanıza yardımcı olmak için kullanılır.

Ardından, arabellek için bayt cinsinden bir boyut girin. 48 baytlık bir arabelleğe ihtiyacınız vardır. Bu arabelleği, 32 bitlik bir kayan noktanın boyutunu ( 4 bayt) vertices dizinizdeki kayan nokta sayısıyla (12) çarparak belirlersiniz. Neyse ki TypedArray'lar byteLength değerlerini sizin için hesaplar. Bu nedenle, arabellek oluştururken bu değeri kullanabilirsiniz.

Son olarak, tamponun kullanım alanını belirtmeniz gerekir. Bu, GPUBufferUsage işaretlerinden biri veya daha fazlasıdır. Birden fazla işaret, | ( bitsel VEYA) operatörüyle birleştirilir. Bu durumda, arabelleğin köşe noktası verileri (GPUBufferUsage.VERTEX) için kullanılmasını ve ayrıca buraya veri kopyalayabilmeyi (GPUBufferUsage.COPY_DST) istediğinizi belirtirsiniz.

Size döndürülen arabellek nesnesi opaktır. Bu nesnenin içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, özelliklerinin çoğu değiştirilemez. GPUBuffer oluşturulduktan sonra yeniden boyutlandıramaz veya kullanım işaretlerini değiştiremezsiniz. Değiştirebileceğiniz tek şey hafızasındaki içeriklerdir.

Arabellek ilk oluşturulduğunda, içerdiği bellek sıfır olarak başlatılır. İçeriğini değiştirmenin birkaç yolu vardır ancak en kolayı, 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 bulunan bir arabellek var ancak GPU açısından bu, yalnızca bir bayt kümesidir. Bu cihazla çizim yapacaksanız biraz daha bilgi vermeniz gerekiyor. WebGPU'ye, 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 durum ilk bakışta biraz kafa karıştırıcı olabilir ancak bu süreci adım adım incelemek oldukça kolaydır.

İlk olarak arrayStride değerini girin. Bu, GPU'nun bir sonraki köşe noktasını ararken arabellekte ileri atlaması gereken bayt sayısıdır. Karenizin her köşesi iki 32 bitlik kayan nokta sayısından oluşur. Daha önce de belirtildiği gibi, 32 bitlik bir kayan nokta 4 bayttır. Dolayısıyla iki kayan nokta 8 bayttır.

Ardından, dizi olan attributes mülkü gelir. Özellikler, her köşe noktasına 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öşenin rengi veya geometri yüzeyinin gösterdiği yön gibi birden fazla özelliğe sahip köşeler bulunur. Ancak bu, bu codelab'in kapsamı dışındadır.

Tek özelliğinizde ilk olarak verilerin format değerini tanımlarsınız. Bu, GPU'nun anlayabileceği her bir köşe noktası verisi türünü açıklayan GPUVertexFormat türlerinin listesinden gelir. Köşelerinizde her biri 32 bitlik iki kayan nokta olduğundan float32x2 biçimini kullanırsınız. Bunun yerine köşe verileriniz her biri dört 16 bitlik işaretsiz tam sayıdan oluşuyorsa bunun yerine uint16x4 değerini kullanırsınız. Bu durumu fark ettiniz mi?

Ardından offset, söz konusu özelliğin köşeden kaç bayt sonra başladığını belirtir. Bu konuda endişelenmeniz gereken tek durum, arabelleğinizde birden fazla özellik olmasıdır. Bu durum bu kod laboratuvarının konusu değildir.

Son olarak shaderLocation'yi 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 özelliği, bir sonraki bölümde bahsedeceğimiz köşe gölgelendiricisindeki belirli bir girişe bağlar.

Bu değerleri şimdi tanımlamış olsanız da henüz WebGPU API'ye aktarmadığınızı unutmayın. Bu konuyu daha sonra ele alacağız. Ancak bu değerleri, köşe noktalarınızı tanımladığınız noktada düşünmek en kolayıdır. Bu nedenle, bunları daha sonra kullanmak üzere şimdiden ayarlıyorsunuz.

Gölgelendiricilerle başlama

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

Gölgelendiriciler, yazdığınız ve GPU'nuzda çalıştırdığınız küçük programlardır. Her gölgelendirici, verilerin farklı bir aşamasında çalışır: Köşe işleme, Parçacık işleme veya genel Hesaplama. GPU'da olduklarından, ortalama JavaScript'inizden daha katı bir şekilde yapılandırılırlar. Ancak bu yapı, çok hızlı ve en önemlisi paralel olarak yürütmelerine olanak tanır.

WebGPU'daki gölgelendiriciler, WGSL (WebGPU Gölgelendirme Dili) adlı bir gölgelendirme dilinde yazılır. WGSL, söz dizimi açısından Rust'a biraz benzer. Sık kullanılan GPU çalışmalarını (ör. vektör ve matris matematikleri) daha kolay ve hızlı hale getirmeyi amaçlayan özelliklere sahiptir. Gölgelendirme dilinin tamamını öğretmek bu codelab'in kapsamının çok ötesindedir. Ancak basit örnekleri inceleyerek temel bilgilerden bazılarını öğrenebilirsiniz.

Gölgelendiriciler, WebGPU'ye dize olarak iletilir.

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

index.html

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

device.createShaderModule() olarak adlandırdığınız gölgelendiricileri oluşturmak için isteğe bağlı bir label ve WGSL code dizesi sağlarsınız. (Çok satırlık dizelere izin vermek için burada ters tırnak kullanmanız gerektiğini 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 düğümü gölgelendiriciyi tanımlama

GPU da buradan başladığı için köşe üstü gölgelendirici ile başlayın.

Köşe üstü gölgelendirici bir işlev olarak tanımlanır ve GPU, vertexBuffer'ünüzdeki her köşe için bu işlevi bir kez çağırır. vertexBuffer'ünüz altı konuma (köşe) sahip olduğundan, tanımladığınız işlev altı kez çağrılır. Her çağrılışında, işleve vertexBuffer'den farklı bir konum bağımsız değişken olarak iletilir ve köşe gölgelendirici işlevinin görevi, klip alanında ilgili konumu döndürmektir.

Bu kişilerin sırayla çağrılmayacağını da unutmayın. 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şleme potansiyeline sahiptir. Bu, GPU'ların inanılmaz hızından sorumlu olan büyük bir faktördür ancak sınırlamaları vardır. Aşırı paralelleştirme sağlamak için köşe üstü gölgelendiriciler birbirleriyle iletişim kuramaz. Her gölgelendirici çağrısı, tek seferde yalnızca tek bir köşe noktasının verilerini görebilir ve yalnızca tek bir köşe noktasının değerlerini döndürebilir.

WGSL'de bir köşe üstü gölgelendirici işlevi istediğiniz şekilde adlandırılabilir ancak hangi gölgelendirici aşamasını temsil ettiğini belirtmek için önünde @vertex özelliği olmalıdır. WGSL, işlevleri fn anahtar kelimesiyle belirtir, bağımsız değişkenleri bildirmek için parantez kullanır ve kapsamı tanımlamak için köşeli parantez 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 üstü gölgelendirici, işlenen köşe üstü için klip alanındaki nihai konumu en azından döndürmelidir. Bu değer her zaman 4 boyutlu bir vektör olarak verilir. Vektörler, gölgelendiricilerde yaygın olarak kullanıldığı için dilde birinci sınıf primitifler olarak kabul edilir ve 4 boyutlu bir vektör için vec4f gibi kendi türlerine sahiptir. 2D vektörler (vec2f) ve 3D vektörler (vec3f) için de benzer türler vardır.

  1. Döndürülen değerin zorunlu konum olduğunu belirtmek için @builtin(position) özelliğiyle işaretleyin. İşlevin döndürdüğü değerin bu olduğunu 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ğerlerinin tümü, döndürülen değerde köşenin kırpma alanında nerede olduğunu belirten kayan nokta sayılarıdır.

  1. Statik bir (0, 0, 0, 1) değeri döndürdüğünüzde teknik olarak geçerli bir köşe üstü gölgelendiriciniz olur. Ancak GPU, oluşturduğu üçgenlerin tek bir nokta olduğunu algılayıp bunları atadığından bu gölgelendirici hiçbir zaman hiçbir şey göstermez.

index.html (createShaderModule kodu)

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

Bunun yerine, oluşturduğunuz arabellekteki verilerden yararlanmak istiyorsunuz. Bunu, işleviniz için vertexBufferLayout içinde tanımladığınızla eşleşen bir @location() özelliği ve türü içeren bir bağımsız değişken açıklayarak yapabilirsiniz. 0 için bir shaderLocation belirttiniz. Bu nedenle, WGSL kodunuzda bağımsız değişkeni @location(0) ile işaretleyin. Ayrıca biçimi 2D vektör olan float32x2 olarak da tanımladınız. Bu nedenle, 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 doğal görünür.

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

index.html (createShaderModule kodu)

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

Şimdi bu konuma dönmeniz gerekiyor. Konum 2D vektör, döndürülen tür ise 4D vektör olduğundan bunu biraz değiştirmeniz gerekir. Yapmanız gereken, konum bağımsız değişkenindeki iki bileşeni alıp bunları döndürülen vektörün ilk iki bileşenine yerleştirmek ve son iki bileşeni 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ğu için konum vektörünü, uygun bir kısaltma olarak 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 tepe noktası gölgelendiriciniz hazır. Bu yöntem çok basittir. Konumun değiştirilmeden dağıtılmasından ibarettir ancak başlangıç için yeterlidir.

Parça gölgelendiriciyi tanımlama

Bir sonraki adım, parçacık gölgelendiricidir. Kırıntı gölgelendiricileri, köşe gölgelendiricilerine çok benzer şekilde çalışır ancak her köşe için çağrılmak yerine çizilen her piksel için çağrılır.

Kırıntı gölgelendiricileri her zaman köşe gölgelendiricilerinden sonra çağrılır. GPU, köşe düğümü gölgelendiricilerinin çıkışını alır ve üç nokta gruplarından üçgenler oluşturarak üçgenleştirir. Ardından, çıkış renk eklerinin hangi piksellerinin bu üçgene dahil olduğunu belirleyerek bu üçgenlerin her birini rasterize eder ve bu piksellerin her biri için bir kez parçacık gölgelendiriciyi çağırır. Kırıntı gölgelendirici, genellikle kendisine köşe gölgelendiricisinden gönderilen değerlerden ve GPU'nun renk eklemesine yazdığı dokular gibi öğelerden hesaplanan bir renk döndürür.

Düğüm gölgelendiricileri gibi, parçacık gölgelendiricileri de büyük ölçüde paralel bir şekilde yürütülür. Giriş ve çıkışları açısından köşe düğümü gölgelendiricilerinden biraz daha esnektirler ancak her üçgenin her pikseli için tek bir renk döndürdüklerini düşünebilirsiniz.

WGSL kırıntı gölgelendirici işlevi, @fragment özelliğiyle gösterilir ve bir vec4f döndürür. Ancak bu durumda vektör, konumu değil rengi temsil eder. Döndürülen rengin, beginRenderPass çağrısındaki hangi colorAttachment değerine yazıldığını belirtmek için dönüş değerine bir @location özelliğinin eklenmesi gerekir. Yalnızca bir ek eklediğiniz için konum 0'dır.

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

index.html (createShaderModule kodu)

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

}

Döndürülen vektörün dört bileşeni kırmızı, yeşil, mavi ve alfa renk değerleridir. Bu değerler, daha önce beginRenderPass içinde ayarladığınız clearValue ile tam olarak aynı şekilde yorumlanır. vec4f(1, 0, 0, 1) parlak kırmızıdır. Bu, kareniz için iyi bir renktir. Yine de 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)
}

İşte tam bir kırıntı gölgelendirici. Çok ilginç bir kod değil. Her üçgenin her pikseli kırmızıya ayarlanıyor. Ancak şimdilik bu yeterli.

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

index.html

const cellShaderModule = device.createShaderModule({
  label: 'Cell shader',
  code: `
    @vertex
    fn vertexMain(@location(0) pos: vec2f) ->
      @builtin(position) vec4f {
      return vec4f(pos, 0, 1);
    }

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

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

Gölgelendirici modülü, tek başına oluşturma için kullanılamaz. Bunun yerine, device.createRenderPipeline() çağrısı yapılarak oluşturulan bir GPURenderPipeline parçası olarak kullanmanız gerekir. Oluşturma ardışık düzeni, hangi gölgelendiricilerin kullanılacağı, köşe çubuğundaki verilerin nasıl yorumlanacağı, hangi tür geometrinin oluşturulacağı (çizgiler, noktalar, üçgenler...) gibi konular da dahil olmak üzere geometrinin nasıl çizileceğini kontrol eder.

Oluşturma ardışık düzeni, API'nin tamamındaki en karmaşık nesnedir ancak endişelenmeyin. Ona iletebileceğiniz değerlerin çoğu isteğe bağlıdır ve başlamak için yalnızca birkaçını sağlamanız gerekir.

  • Şuna benzer bir oluşturma ardışık düzeni oluşturun:

index.html

const cellPipeline = device.createRenderPipeline({
  label: "Cell pipeline",
  layout: "auto",
  vertex: {
    module: cellShaderModule,
    entryPoint: "vertexMain",
    buffers: [vertexBufferLayout]
  },
  fragment: {
    module: cellShaderModule,
    entryPoint: "fragmentMain",
    targets: [{
      format: canvasFormat
    }]
  }
});

Her ardışık düzenin, ardışık düzenin ihtiyaç duyduğu giriş türlerini (köşe çubuğu hariç) açıklayan bir layout öğesine ihtiyacı vardır ancak böyle bir öğeniz yoktur. Neyse ki şimdilik "auto" değerini iletebilirsiniz. Bu durumda ardışık düzenleyici, gölgelendiricilerden kendi düzenini oluşturur.

Ardından vertex aşamasıyla ilgili ayrıntıları sağlamanız gerekir. module, köşe üstü gölgelendiricinizi içeren GPUShaderModule'dir ve entryPoint, gölgelendirici kodunda her köşe üstü çağrısı için çağrılan işlevin adını verir. (Tek bir gölgelendirici modülünde birden fazla @vertex ve @fragment işlevi olabilir.) Arabellekler, verilerinizin bu ardışık düzeni kullandığınız köşe çubuğu arabelleklerinde nasıl paketlendiğini açıklayan bir GPUVertexBufferLayout nesnesi dizisidir. Neyse ki bunu daha önce vertexBufferLayout'ünüzde tanımlamıştınız. Burada, bu bilgileri gönderirsiniz.

Son olarak fragment aşamasıyla ilgili ayrıntılar yer alır. Buna bir gölgelendirici modülü ve tepe noktası aşaması gibi giriş noktası da dahildir. Son olarak, bu ardışık düzenin kullanıldığı targets öğesini tanımlamanız gerekir. Bu, ardışık düzenin çıktı olarak verdiği renk ekleriyle ilgili ayrıntıları (ör. doku format) içeren bir sözlük dizisidir. Bu ayrıntıların, bu ardışık düzenin kullanıldığı tüm oluşturma geçişlerinin colorAttachments bölümünde verilen dokularla eşleşmesi gerekir. Oluşturma geçişiniz, tuval bağlamındaki dokuları kullanır ve biçimi için canvasFormat'te kaydettiğiniz değeri kullanır. Bu nedenle, burada da aynı biçimi iletirsiniz.

Bu, oluşturma ardışık düzeni oluştururken belirtebileceğiniz seçeneklerin tamamına yakını olmasa da bu codelab'in ihtiyaçları için yeterlidir.

Kareyi çizme

Böylece karenizi çizmek için ihtiyacınız olan her şeye sahip oldunuz.

  1. Kare çizmek için encoder.beginRenderPass() ve pass.end() çağrı çiftine geri dönün ve aralarında ş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 sayede WebGPU, karenizi çizmek için gerekli tüm bilgileri alır. Öncelikle, çizim için hangi ardışık düzenin kullanılacağını belirtmek üzere setPipeline() simgesini kullanırsınız. Kullanılan gölgelendiriciler, köşe noktası verilerinin düzeni ve diğer ilgili durum verileri buna dahildir.

Ardından, kareniz için köşe noktalarını içeren arabellekle setVertexBuffer() işlevini çağırırsınız. Bu arabellek, geçerli ardışık düzenin vertex.buffers tanımındaki 0. öğeye karşılık geldiği için 0 ile çağırırsınız.

Son olarak, draw() aramasını yaparsınız. Bu arama, önceki tüm kurulumlardan sonra garip bir şekilde basit görünür. İletmek için tek yapmanız gereken, oluşturması gereken köşe sayısıdır. Bu sayı, şu anda ayarlanmış köşe tamponlarından alınır ve şu anda ayarlanmış ardışık düzen ile yorumlanır. Bunu 6 olarak sabit kodlayabilirsiniz ancak köşeler dizisinden hesaplamak (köşe başına 12 kayan nokta / 2 koordinat == 6 köşe), kareyi örneğin bir daireyle değiştirmeye karar verirseniz manuel olarak güncellemeniz gereken öğe sayısını azaltır.

  1. Ekranınızı yenileyin ve tüm emeklerinizin karşılığını 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. Geometrinin ilk parçalarını ekrana yansıtmak, çoğu GPU API'sinde genellikle en zor adımlardan biridir. Buradan yapacağınız tüm işlemler daha küçük adımlarla yapılabilir. Bu sayede, ilerlemenizi daha kolay doğrulayabilirsiniz.

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

  • JavaScript'ten gölgelendiriciye değişkenler (uniform olarak adlandırılır) nasıl aktarılır?
  • Oluşturma davranışını değiştirmek için tek tipleri kullanma.
  • Aynı geometrinin birçok farklı varyantını çizmek için örneklemeyi kullanma.

Izgara tanımlama

Bir ızgara oluşturmak için ızgara hakkında çok temel bir bilgiyi bilmeniz gerekir. Hem genişlik hem de 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 ikinin kuvveti 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 isteyeceksiniz ancak bu bölümde kullanılan bazı matematik işlemlerini daha kolay göstermek için bu bölümün geri kalanında ızgara boyutunu 4x4 olarak ayarlayın. Daha sonra ölçeği artırın.

  • JavaScript kodunuzun üst kısmına bir sabit ekleyerek ızgara boyutunu tanımlayın.

index.html

const GRID_SIZE = 4;

Ardından, karenizi GRID_SIZE x GRID_SIZE boyutunda tuvale sığdırabilmek için karenizi oluşturma şeklinizi güncellemeniz gerekir. Yani karenin çok daha küçük olması ve çok sayıda karenin olması gerekir.

Bu soruna yaklaşabileceğiniz bir yöntem, köşe ara belleğinizi önemli ölçüde büyütmek ve içinde GRID_SIZE x GRID_SIZE kare tanımlamak, bunları doğru boyut ve konumda yerleştirmektir. Bunun için kod yazmak aslında çok da zor değil. Birkaç for döngüsü ve biraz matematik yeterlidir. Ancak bu, GPU'dan en iyi şekilde yararlanmamakta ve efekti elde etmek için gerekenden daha fazla bellek kullanmaktadır. Bu bölümde, GPU'ya daha uygun bir yaklaşım ele alınmaktadır.

Tekdüze bir arabellek oluşturma

Öncelikle, seçtiğiniz ızgara boyutunu gölgelendiriciye iletmeniz gerekir. Gölgelendirici, öğelerin nasıl gösterileceğini değiştirmek için bu boyutu kullanır. Boyutu gölgelendiriciye sabit kod olarak ekleyebilirsiniz. Ancak bu durumda, ızgara boyutunu değiştirmek istediğinizde gölgelendiriciyi ve oluşturma ardışık düzenini yeniden oluşturmanız gerekir. Bu da pahalıdır. Daha iyi bir yöntem, ızgara boyutunu gölgelendiriciye üniforma olarak sağlamaktır.

Daha önce, bir köşe düğümü gölgelendiricisinin her çağrılmasında köşe düğümü arabelleğinden farklı bir değerin iletildiğini öğrenmiştiniz. Tekdüzen, her çağrı için aynı olan bir arabellekteki değerdir. Bu özellikler, bir geometri parçası (ör. konumu), animasyon çerçevesinin tamamı (ör. mevcut saat) veya hatta uygulamanın tüm yaşam döngüsü (ör. kullanıcı tercihi) için ortak olan değerleri iletmek amacıyla kullanışlıdır.

  • Aşağıdaki kodu ekleyerek tekdüze 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 ara belleği oluşturmak için kullandığınız koda neredeyse tamamen aynı olduğu için size çok tanıdık gelecektir. Bunun nedeni, üniformaların WebGPU API'ye, köşe noktalarının bulunduğu GPUBuffer nesneleri aracılığıyla iletilmesidir. Buradaki temel fark, bu seferki usage değerinin GPUBufferUsage.VERTEX yerine GPUBufferUsage.UNIFORM içermesidir.

Gölgelendiricideki tek biçimlere erişim

  • Aşağıdaki kodu ekleyerek bir üniforma 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 üniforma tanımlar. Bu üniforma, üniforma arabelleğine kopyaladığınız dizi ile eşleşen 2D kayan vektördür. Ayrıca üniformanın @group(0) ve @binding(0)'te bağlandığını 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 bir 2D vektör ve grid bir 2D vektör olduğundan WGSL bileşen bazında bölme işlemi gerçekleştirir. Diğer bir deyişle, sonuç vec2f(pos.x / grid.x, pos.y / grid.y) ile aynıdır.

Birçok oluşturma ve hesaplama tekniği bu tür vektör işlemlerini kullandığından GPU gölgelendiricilerinde bu tür işlemler çok yaygındır.

Bu durum sizin durumunuzda, (4 boyutunda bir ızgara kullandıysanız) oluşturacağınız 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 mükemmel bir seçenektir.

Bağlantı grubu oluşturma

Ancak uniform'u gölgelendiricide belirtmek, onu oluşturduğunuz arabelleğe bağlamaz. Bunun için bir bağlantı grubu oluşturup ayarlamanız gerekir.

Bağlama grubu, gölgelendiricinizin aynı anda erişebilmesini istediğiniz kaynaklardan oluşan bir koleksiyondur. Tekdüze arabelleğiniz gibi çeşitli arabellek türleri ve burada ele alınmayan ancak WebGPU oluşturma tekniklerinin ortak parçaları olan dokular ve örnekleyiciler gibi diğer kaynakları içerebilir.

  • Tekdüzen arabellek ve oluşturma ardışık düzeni oluşturulduktan sonra aşağıdaki kodu 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'unuza ek olarak, bu bağlama grubunun hangi tür kaynakları içerdiğini açıklayan bir layout'a da ihtiyacınız vardır. Bu konuyu ilerideki bir adımda daha ayrıntılı olarak inceleyeceksiniz ancak şu anda layout: "auto" ile oluşturduğunuz için ardışık düzeninizin bağlama grubu düzenini sorabilirsiniz. Bu, ardışık düzenin, gölgelendirici kodunda beyan ettiğiniz bağlamalardan otomatik olarak bağlama grubu düzenleri oluşturmasına neden olur. Bu durumda, 0'un gölgelendiriciye yazdığınız @group(0) değerine karşılık geldiği getBindGroupLayout(0) değerini alırsınız.

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

  • binding, gölgelendiriciye girdiğiniz @binding() değerine karşılık gelir. Bu durumda 0.
  • resource: Belirtilen bağlama dizininde değişkene göstermek istediğiniz gerçek kaynaktır. Bu durumda, tekdüze arabelleğiniz.

İşlev, opak ve değiştirilemez bir tutma yeri olan bir GPUBindGroup döndürür. Bir bağlama grubunun işaret ettiği kaynakları oluşturulduktan sonra değiştiremezsiniz ancak bu kaynakların içeriklerini değiştirebilirsiniz. Örneğin, tekdüze arabelleği yeni bir ızgara boyutu içerecek şekilde değiştirirseniz bu, bu bağlama grubunu kullanan gelecekteki çizim çağrılarına yansıtılır.

Bağlama grubunu bağlama

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

  1. Oluşturma 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)'un parçası olan her @binding'nin bu bağlama grubundaki kaynakları kullandığını söylüyorsunuz.

Artık tekdüze arabellek, gölgelendiricinize açıktır.

  1. Sayfanızı yenileyin. Ardından aşağıdakine benzer bir ekran görürsünüz:

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

Yaşasın! Kareniz artık önceki boyutunun dörtte biri kadar. Bu çok fazla bir şey değil ancak üniformanızın gerçekten uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.

Gölgelendiricide geometriyi değiştirme

Şimdi gölgelendiricide ızgara boyutuna referans verebiliyorsunuz. Bu nedenle, oluşturmakta olduğunuz geometriyi istediğiniz ızgara desenine uyacak şekilde değiştirmek için bazı çalışmalar yapmaya başlayabilirsiniz. Bunu yapmak için tam olarak neyi başarmak istediğinizi düşünün.

Kanvasınızı kavramsal olarak ayrı hücrelere ayırmanız gerekir. Sağa doğru ilerledikçe X ekseninin, yukarı doğru ilerledikçe Y ekseninin arttığı kuralını korumak için ilk hücrenin kanvasın sol alt köşesinde olduğunu varsayalım. Bu işlem, ortasında mevcut kare geometriniz bulunan aşağıdaki gibi bir düzen oluşturur:

Normalleştirilmiş cihaz koordinatı alanının, her hücrenin merkezindeki geçerli olarak oluşturulmuş kare geometriyle görselleştirilirken bölüneceği kavramsal ızgaranın bir resmi.

Göreviniz, gölgelendiricide hücre koordinatları verilen bu hücrelerden herhangi birinde kare geometrisini konumlandırmanıza olanak tanıyan bir yöntem bulmaktır.

Öncelikle, kareniz tuvalin ortasını çevreleyecek şekilde tanımlandığı için hücrelerden hiçbiriyle düzgün bir şekilde hizalanmadığını görebilirsiniz. Kare, içlerinde düzgün bir şekilde hizalanabilmesi için yarım hücre kaydırılmalıdır.

Bu sorunu düzeltmenin bir yolu, karenin köşe ara belleğini güncellemektir. Köşeleri, sol alt köşe (-0,8, -0,8) yerine örneğin (0,1, 0,1) olacak şekilde kaydırarak bu kareyi hücre sınırlarıyla daha iyi hizalanacak şekilde hareket ettirebilirsiniz. Ancak, tepe noktalarının gölgelendiricinizde nasıl işlendiği üzerinde tam kontrole sahip olduğunuzdan, gölgelendirici kodunu kullanarak bunları kolayca yerine yerleştirebilirsiniz.

  1. Köşe düğümü 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 boyutuna bölmeden önce bir birim yukarı ve sağa taşır (bu değerin, kırpma alanının yarısı olduğunu unutmayın). Sonuç, orijinin hemen yanındaki ızgaraya hizalanmış güzel bir karedir.

(2, 2) hücresinde kırmızı bir kare bulunan, kavramsal olarak 4x4 ızgaraya bölünmüş kanvasın görselleştirmesi

Ardından, tuvalinizin koordinat sistemi (0, 0) değerini ortada, (-1, -1) değerini ise sol alt köşede yerleştirir. Siz de (0, 0) değerinin sol alt köşede olmasını istediğiniz için geometrinizin konumunu, ızgara boyutuna bölerek sonra (-1, -1) değerine göre çevirmeniz gerekir. Böylece geometrinizi bu köşeye taşıyabilirsiniz.

  1. Geometrinizin konumunu şu şekilde çevirin:

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

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

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

  // Subtract 1 after dividing by the grid size.
  let gridPos = (pos + 1) / grid - 1;

  return vec4f(gridPos, 0, 1);
}

Böylece kareniz (0, 0) hücresine güzelce yerleştirilmiş olur.

Kavramsal olarak 4x4 ızgaraya bölünmüş tuvalin görselleştirmesi. (0, 0) hücresinde kırmızı bir kare var.

Farklı bir hücreye yerleştirmek isterseniz ne olur? Bunu, gölgelendiricinizde bir cell vektörü tanımlayıp let cell = vec2f(1, 1) gibi statik bir değerle doldurarak anlayabilirsiniz.

Bunu gridPos'e eklerseniz algoritmadaki - 1 işlemi geri alınır. Bu nedenle, bunu yapmak istemezsiniz. Bunun yerine, kareyi her hücre için yalnızca bir ızgara birimi (tuvalin dörtte biri) kadar taşımak istiyorsunuz. grid'e göre başka bir bölme işlemi yapmanız 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);
}

Sayfayı hemen yenilediğinizde aşağıdakileri görürsünüz:

(0, 0) hücresi, (0, 1) hücresi, (1, 0) hücresi ve (1, 1) hücresinin ortasına yerleştirilmiş kırmızı bir karenin bulunduğu, kavramsal olarak 4x4 ızgaraya bölünmüş kanvasın görselleştirmesi

Hımm. Tam olarak istediğiniz gibi değil.

Bunun nedeni, tuval koordinatlarının -1 ile +1 arasında değiştiğinden aslında 2 birim genişliğinde olmasıdır. Yani bir köşeyi kanvasın dörtte biri kadar taşımak istiyorsanız 0, 5 birim taşımanız gerekir. GPU koordinatlarını kullanırken bu hatayı yapmak kolaydır. Neyse ki bu sorunu düzeltmek de o kadar kolay.

  1. Ofsetinizi 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);
}

Böylece tam olarak istediğinizi elde edersiniz.

Kavramsal olarak 4x4 ızgaraya bölünmüş tuvalin görselleştirmesi. (1, 1) hücresinde kırmızı bir kare var.

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ığı şekilde aynı konumda çizilir ancak ızgara yer paylaşımı olmadan çizilir.

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şturmak için sayfayı yenileyebilirsiniz.

Örnek çizme

Bir miktar matematikle kareyi istediğiniz yere yerleştirebildiğinize göre, sonraki adımda ızgaranın her hücresine bir kare oluşturmanız gerekir.

Bu soruna yaklaşmanın bir yolu, hücre koordinatlarını tekdüze bir arabelleğe yazmak, ardından draw işlevini ızgaradaki her kare için bir kez çağırarak her seferinde tekdüzeliği güncellemektir. 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 tekniği kullanabilirsiniz. Örnekleme, GPU'ya draw çağrısı yaparak aynı geometrinin birden fazla kopyasını çizmesini söylemenin bir yoludur. Bu yöntem, her kopya için draw çağrısı yapmaktan çok daha hızlıdır. Geometrinin her kopyasına örnek adı verilir.

  1. GPU'ya, karenizi ızgaranın tamamını dolduracak kadar örnekle istediğinizi bildirmek için mevcut çizim çağrınıza bir bağımsız değişken ekleyin:

index.html

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

Bu, sisteme karenizdeki altı (vertices.length / 2) köşeyi 16 (GRID_SIZE * GRID_SIZE) kez çizmesini istediğinizi bildirir. Ancak sayfayı yenilediğinizde aşağıdakileri görmeye devam edersiniz:

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

Neden? Bunun nedeni, bu karelerin 16'sını da aynı yere çizmenizdir. Gölgelendiricide, geometriyi örnek başına yeniden konumlandıran ek bir mantığa ihtiyacınız vardır.

Gölgelendiricide, pos gibi köşe çubuğunuzdan gelen köşe özelliklerine ek olarak WGSL'nin yerleşik değerleri olarak bilinen değerlere de erişebilirsiniz. Bunlar WebGPU tarafından hesaplanan değerlerdir ve bu değerlerden biri instance_index'tür. instance_index, gölgelendirici mantığınızın bir parçası olarak kullanabileceğiniz 0 ile number of instances - 1 arasında değişen, işaretsiz bir 32 bitlik sayıdır. Değeri, aynı örneğin parçası olan ve işlenen her köşe için aynıdır. Bu, köşe çubuğundaki her konum için bir kez olmak üzere köşe gölgelendiricinizin 0 değerine sahip bir instance_index ile altı kez çağrılacağı anlamına gelir. Ardından 1 için instance_index ile altı kez daha, 2 için instance_index ile altı kez daha ve bu şekilde devam edin.

Bunun nasıl çalıştığını görmek için instance_index yerleşik işlevini gölgelendirici girişlerinize eklemeniz gerekir. Bunu, konumla aynı şekilde yapın ancak @location özelliğiyle etiketlemek yerine @builtin(instance_index) kullanın ve ardından bağımsız değişkeni istediğiniz şekilde adlandırın. (Örnek koda uyması 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);
}

Sayfayı şimdi yenilediğinizde birden fazla kareniz olduğunu görebilirsiniz. Ancak bunların 16'sını da göremezsiniz.

Koyu mavi bir arka planda sol alt köşeden sağ üst köşeye çapraz bir çizgi halinde 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ıdır. Ancak bunların yalnızca ilk dördü tuvale sığar. İstediğiniz ızgarayı oluşturmak için instance_index değerini, her dizin ızgaranızdaki benzersiz bir hücreyle eşleşecek şekilde dönüştürmeniz gerekir. Örneğin:

Tuvalin, her hücrenin doğrusal bir örnek diziniyle de ilişkilendirildiği, kavramsal olarak 4x4 ızgaraya bölünmüş bir görselleştirmesi.

Bu hesaplama oldukça basittir. Her hücrenin X değeri için instance_index değerinin modülü ve % operatörüyle WGSL'de gerçekleştirebileceğiniz ızgara genişliğini istiyorsunuz. Ayrıca, her hücrenin Y değeri için instance_index değerinin, kalan kesirli kısmı atlayarak ızgara genişliğine bölünmesini istiyorsunuz. Bunu WGSL'nin floor() işleviyle yapabilirsiniz.

  1. Hesaplamaları şu şekilde 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);
}

Kodda bu güncellemeyi yaptıktan sonra nihayet uzun zamandır beklediğiniz kare ızgarasına sahip oldunuz.

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

  1. Artık çalışıyor. Geri dönüp ızgara boyutunu artırın.

index.html

const GRID_SIZE = 32;

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

İşte bu kadar. Artık bu ızgaraları çok büyük yapabilirsiniz ve ortalama bir GPU bunu sorunsuz şekilde yönetir. GPU performansıyla ilgili darboğazlar yaşamadan çok önce kareleri tek tek görmeyi bırakırsınız.

6. Ek kredi: Daha renkli hale getirin.

Bu noktada, kod laboratuvarının geri kalanının temelini attığınız için kolayca bir sonraki bölüme geçebilirsiniz. Ancak aynı rengi paylaşan karelerden oluşan bir ızgara kullanışlı olsa da pek heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla işleri biraz daha parlak hale getirebilirsiniz.

Yapıları gölgelendiricilerde kullanma

Şimdiye kadar, köşe düğümü gölgelendiricisinden bir veri parçası aktardınız: dönüştürülmüş konum. Ancak aslında, köşe düğümü gölgelendiricisinden çok daha fazla veri döndürebilir ve ardından bunları parçacık gölgelendiricide kullanabilirsiniz.

Verileri köşe düğümü gölgelendiricisinden aktarmanın tek yolu döndürmektir. Bir konumu döndürmek için her zaman bir köşe düğümü gölgelendirici gerekir. Bu nedenle, konumla birlikte başka veriler döndürmek istiyorsanız bunları bir yapıya 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ı herhangi bir işlevin dışında tanımlarsınız ve daha sonra gerektiğinde örneklerini işlevlere aktarabilirsiniz. Örneğin, mevcut köşe düğümü 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 dizinlerine input ile başvurmanız gerektiğini ve döndürdüğünüz yapının önce bir değişken olarak tanımlanması ve ayrı ayrı özelliklerinin ayarlanması gerektiğini unutmayın. Bu durumda, çok fazla fark yaratmaz ve aslında gölgelendirici işlevini biraz daha uzatır. Ancak gölgelendiricileriniz daha karmaşık hale geldikçe yapıları kullanmak, verilerinizi düzenlemenize yardımcı olacak mükemmel bir yol olabilir.

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

@fragment işlevinizi mümkün olduğunca basit tutmanız gerektiğini hatırlatmak isteriz:

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

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

Hiçbir giriş almıyorsunuz ve çıkış olarak tek renk (kırmızı) gönderiyorsunuz. Ancak gölgelendirici, renklendirdiği geometri hakkında daha fazla bilgi sahibi olsaydı bu ek verileri kullanarak işleri biraz daha ilgi çekici hale getirebilirdiniz. Örneğin, her karenin rengini hücre koordinatına göre değiştirmek isterseniz ne yapabilirsiniz? @vertex aşaması hangi hücrenin oluşturulduğunu bilir. @fragment aşamasına iletmeniz yeterlidir.

Verileri köşe ve parça aşamaları arasında iletmek için seçtiğiniz bir @location ile çıkış yapısına dahil etmeniz gerekir. Hücre koordinatını iletmek istediğiniz için daha önceki VertexOutput yapısını ekleyin ve döndürmeden önce @vertex işlevinde ayarlayın.

  1. Köşe yayıcınızın döndürülen 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 takip etmeniz 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ı 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 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, değerleri aktarmayı kolaylaştırır.

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

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

Hangi kalıbı seçerseniz seçin, sonuç olarak @fragment işlevindeki hücre numarasına erişebilir ve rengi etkilemek için bu numarayı kullanabilirsiniz. Yukarıdaki kodlardan herhangi birinde çıkış şu şekilde görünür:

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

Artık daha fazla renk var ancak bu durum pek de hoş bir görünüm oluşturmuyor. Neden yalnızca sol ve alt satırların 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 tüm değerlerin bu aralığa kaydırmasıdır. Öte yandan, hücre değerleriniz her eksen boyunca 0 ile 32 arasında değişir. Burada gördüğünüz şey, ilk satır ve sütunun hemen kırmızı veya yeşil renk kanalında 1 değerine ulaştığı ve bundan sonraki her hücrenin aynı değere sabitlendiğidir.

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 eksen boyunca sıfırdan başlayıp bir ile biten bir değer döndürmeniz gerekir. Bu da grid'e bölme işleminin tekrarlanması anlamına gelir.

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

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

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

Sayfayı yenilediğinizde, yeni kodun tüm ızgara boyunca çok daha güzel bir renk gradyanı oluşturduğunu 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 sol alt köşede ızgaranın siyahlaştığı talihsiz bir karanlık köşe var. Yaşam Oyunu simülasyonunu başlattığınızda, ızgaranın zor görülebilen bir bölümü, neler olduğunu gizleyecektir. Bu konuyu açıklığa kavuşturalım.

Neyse ki kullanabileceğiniz kullanılmayan bir renk kanalınız (mavi) var. İdeal olarak, diğer renklerin en koyu olduğu yerde mavi rengin en parlak olmasını, ardından diğer renklerin yoğunluğu arttıkça mavi rengin soluklaşmasını istiyorsunuz. 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 deneyin ve ardından tercih ettiğinizi seçin.

  1. Parçacık gölgelendiriciye aşağıdaki gibi daha parlak renkler ekleyin:

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, maviye ve 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 eklendi ve bu kod laboratuvarındaki diğer ekran görüntüleri bu daha renkli ızgarayı yansıtıyor.

7. Hücre durumunu yönetme

Ardından, GPU'da depolanan bazı durumlara bağlı olarak, ı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/kapatma sinyalidir. Bu nedenle, neredeyse her değer türünü içeren büyük bir dizi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun, tekdüze tamponların başka bir kullanım alanı olduğunu düşünebilirsiniz. Bu yöntemi kullanabilir olsanız da tekdüze arabelleklerin boyutu sınırlı olduğundan, dinamik boyutlu dizileri destekleyemediğinden (dizi boyutunu gölgelendiricide belirtmeniz gerekir) ve hesaplama gölgelendiricileri tarafından yazılamadığından daha zordur. Yaşam Oyunu simülasyonunu GPU'da bir işleme gölgelendiricisinde yapmak istediğiniz için en sorunlu olan son öğedir.

Neyse ki bu sınırlamaların hiçbirine takılmayan başka bir arabellek seçeneği var.

Depolama alanı arabelleği oluşturma

Depolama arabellekleri, işleme gölgelendiricilerinde okunup yazılabilen ve köşe gölgelendiricilerinde okunabilen genel amaçlı arabelleklerdir. Bunlar çok büyük olabilir ve bir gölgelendiricide belirli bir boyut beyan etmelerine gerek yoktur. Bu da onları genel belleğe çok daha benzer hale getirir. Hücre durumunu depolamak için bunu kullanırsınız.

  1. Hücre durumunuz için bir depolama tamponu oluşturmak üzere, şu anda muhtemelen tanıdık görünmeye başlayan tampon oluşturma kodu snippet'ini 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,
});

Tıpkı köşe ve tekdüze arabelleklerinizde olduğu gibi, device.createBuffer() işlevini uygun boyutta çağırın ve bu kez GPUBufferUsage.STORAGE işlevinin kullanımını belirttiğinizden emin olun.

Aynı boyuttaki TypedArray'ı değerlerle doldurup device.queue.writeBuffer()'yi çağırarak arabelleği daha önce olduğu gibi doldurabilirsiniz. Arabelleğinizin ızgara üzerindeki etkisini görmek istediğiniz için önce arabelleği tahmin edilebilir bir öğeyle doldurun.

  1. Aşağıdaki kodla her üç 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ğini inceleyecek şekilde gölgelendiricinizi güncelleyin. Bu, daha önce üniformaların eklenme şekline çok benziyor.

  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, ızgara üniformasının hemen altına yerleştirilen bağlama noktasını ekleyin. @group değerini grid üniformasıyla aynı tutmak istiyorsunuz ancak @binding sayısının farklı olması gerekiyor. Farklı türde bir arabellek yansıtmak için var türü storage'tür ve JavaScript'teki Uint32Array ile eşleşecek şekilde cellState için verdiğiniz tür tek bir vektör yerine bir u32 değeri dizisidir.

Ardından, @vertex işlevinizin gövdesinde hücrenin durumunu sorgulayın. Durum, depolama arabelleğinde düz bir dizi halinde depolandığından, geçerli hücrenin değerini aramak için instance_index değerini kullanabilirsiniz.

Durum etkin değilse bir 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'e ölçeklendirmek geometriyi olduğu gibi bırakır, 0'a ölçeklendirmek ise geometriyi tek bir noktaya çökertir ve GPU bu noktayı atar.

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

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 alanı arabelleğini bağlama grubuna ekleme

Hücre durumunun geçerli hale geldiğini görebilmeniz için depolama arabelleğini bir bağlama grubuna ekleyin. Tekdüzen arabellekle aynı @group'nin parçası olduğu için JavaScript kodunda da aynı bağlama grubuna ekleyin.

  • Depolama alanı tamponunu şu şekilde 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şlemi tamamladıktan sonra, yenilemeniz ve ızgaradaki kalıbı görebilmeniz gerekir.

Koyu mavi bir arka planda soldan sağa doğru giden renkli karelerden oluşan çapraz şeritler.

Ping-pong arabelleği düzenini kullanma

Oluşturduğunuz gibi çoğu simülasyon, genellikle durumlarının en az iki kopyasını kullanır. Simülasyonun her adımında, durumun bir kopyasından okuma yapar ve diğerine yazar. Ardından, bir sonraki adımda kartı çevirin ve daha önce yazdıkları yerden okuyun. Durumun en güncel sürümü her adımda durum kopyaları arasında ileri geri gidip geldiğinden bu duruma genellikle ping pong denir.

Bu neden gerekli? Basitleştirilmiş bir örneğe göz atın: Her adımda etkin blokları sağa bir hücre taşıdığınız çok basit bir simülasyon yazdığınızı hayal edin. İşlemlerin kolay anlaşılır olması için verilerinizi ve simülasyonunuzu JavaScript'te tanımlarsınız:

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

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

simulate(); // Run the simulation for one step.

Ancak bu kodu çalıştırırsanız etkin hücre tek adımda dizinin sonuna kadar hareket eder. Neden? Durumu yerinde güncellemeye devam ettiğiniz için etkin hücreyi sağa taşırsınız ve bir sonraki hücreye bakarsınız. Etkin. Tekrar sağa hareket ettirin. Verileri gözlemlediğiniz sırada değiştirmeniz sonuçları bozar.

Ping pong modelini kullanarak, her zaman simülasyonun bir sonraki adımını yalnızca son adımın sonuçlarını kullanarak gerçekleştirdiğinizden emin olursunuz.

// 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 aynı arabellek oluşturmak için depolama arabellek atamanı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 bunları farklı verilerle doldurun:

index.html

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

// Mark every other cell of the second grid as active.
for (let i = 0; i < cellStateArray.length; i++) {
  cellStateArray[i] = i % 2;
}
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
  1. Oluşturma işleminizde farklı depolama alanı arabelleklerini göstermek için bağlama gruplarınızı da iki farklı varyant içerecek şekilde güncelleyin:

index.html

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

Oluşturma döngüsü oluşturma

Şimdiye kadar sayfa yenileme başına yalnızca bir çizim 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ü gerekir.

Oluşturma döngüsü, içeriğinizi belirli bir aralıklarla tuvale çizen, sonsuz şekilde tekrarlanan bir döngüdür. Sorunsuz animasyonlar oluşturmak isteyen birçok oyun ve diğer içerik, geri çağırma işlemlerini ekranın yenilendiği hızda (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 edebilmeniz için güncellemelerin daha uzun adımlarla yapılmasını istersiniz. Bunun yerine, simülasyonunuzun güncellenme hızını kontrol etmek için döngüyü kendiniz yönetin.

  1. Öncelikle, simülasyonumuzun güncelleneceği bir hız seçin (200 ms iyi bir değerdir ancak isterseniz daha yavaş veya daha hızlı gidebilirsiniz). Ardından, simülasyonun kaç adımını tamamladığınızı takip edin.

index.html

const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
  1. Ardından, oluşturma işlemi için şu anda kullandığınız 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 iki bağlama grubundan hangisinin bağlanacağını seçmek için bunu 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);

Artık uygulamayı çalıştırdığınızda kanvasın, oluşturduğunuz iki durum arabelleğini göstermek için ileri geri döndüğünü görürsünüz.

Koyu mavi bir arka plan üzerinde soldan sağa doğru giden renkli karelerden oluşan çapraz şeritler. Koyu mavi arka plan üzerinde renkli karelerden oluşan dikey şeritler.

Bu işlemle birlikte, oluşturma işleminin büyük bir kısmını tamamlamış olursunuz. Bir sonraki adımda oluşturacağınız Yaşam Oyunu simülasyonunun çıktısını görüntülemeye hazırsınız. Bu adımda, hesaplama gölgelendiricileri kullanmaya başlayacaksınız.

WebGPU'nun oluşturma özelliklerinde, burada keşfettiğiniz küçük dilimden çok daha fazlası vardır ancak geri kalanı bu kod laboratuvarının kapsamı dışındadır. Bu makale, WebGPU'nun oluşturma işleminin nasıl çalıştığına dair yeterli bilgi vermeyi ve 3D oluşturma gibi daha gelişmiş teknikleri daha kolay anlamanıza yardımcı olmayı umuyor.

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

Şimdi, bulmacanın son önemli parçasına geçelim: Game of Life simülasyonunu bir hesaplama gölgelendiricisinde gerçekleştirme.

Hesaplama gölgelendiricileri nihayet kullanıma sunuldu.

Bu codelab boyunca, işleme gölgelendiricileri hakkında soyut bir şekilde bilgi edindiniz. Peki bunlar tam olarak nedir?

İşlem gölgelendiricileri, GPU'da aşırı paralellikle çalışacak şekilde tasarlandıkları için köşe ve parçacık gölgelendiricilere benzer ancak diğer iki gölgelendirici aşamasının aksine belirli bir giriş ve çıkış grubuna sahip değildir. Verileri yalnızca seçtiğiniz kaynaklardan (ör. depolama alanı arabellekleri) okuyor ve yazıyorsunuz. Bu, her köşe noktası, örnek veya piksel için bir kez yürütmek yerine, istediğiniz 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ğrının işlendiği size bildirilir ve buradan hangi verilere erişeceğinize ve hangi işlemleri yapacağınıza karar verebilirsiniz.

Hesaplama gölgelendiricileri, tıpkı köşe ve parçacık gölgelendiricileri gibi bir gölgelendirici modülünde oluşturulmalıdır. Bu nedenle, başlamak için bu modülü kodunuza ekleyin. Tahmin edebileceğiniz gibi, 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 gerekir.

  1. Aşağıdaki kodla 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ıklıkla kullanıldığından, işleme gölgelendiricileri, gölgelendiricinin X, Y ve Z ekseni boyunca belirli sayıda çağrılmasını isteyebileceğiniz şekilde yapılandırılır. Bu sayede, 2D veya 3D ızgaraya uygun çalışmaları çok kolay bir şekilde dağıtabilirsiniz. Bu, kullanım alanınız için mükemmel bir özelliktir. Bu gölgelendiriciyi, simülasyonunuzun her hücresi için bir kez olmak üzere GRID_SIZE kez GRID_SIZE kez çağırmak istiyorsunuz.

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

  1. İş grubunuzun boyutu için aşağıdaki gibi bir sabit tanımlayın:

index.html

const WORKGROUP_SIZE = 8;

Ayrıca, çalışma grubu boyutunu gölgelendirici işlevine eklemeniz gerekir. Bunu, yeni tanımladığınız sabit değeri kolayca kullanabilmek için JavaScript'in şablon literallerini kullanarak yaparsınız.

  1. Çalışma grubu boyutunu gölgelendirici işlevine şu şekilde ekleyin:

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

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

}

Bu, gölgelendiriciye bu işlevle yapılan çalışmanın (8 x 8 x 1) gruplar halinde yapıldığını bildirir. (En azından X eksenini belirtmeniz gerekir ancak atladığınız tüm eksenler varsayılan olarak 1 olur.)

Diğer gölgelendirici aşamalarında olduğu gibi, hangi çağrıyı yaptığınızı öğrenmek ve ne 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. Aşağıdaki gibi bir @builtin değeri ekleyin:

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

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

}

global_invocation_id yerleşik işlevini iletin. Bu işlev, gölgelendirici çağrısı ızgarasında nerede olduğunuzu belirten, üç boyutlu, işaretsiz tam sayılardan oluşan bir vektördür. Bu gölgelendiriciyi, ızgaranızdaki 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)'a kadar olan sayılar alırsınız. Bu sayede, bu sayıyı üzerinde işlem yapacağınız hücre dizini olarak kullanabilirsiniz.

İşlem gölgelendiricileri, tıpkı köşe ve parçacık gölgelendiricilerinde kullandığınız gibi üniformalar da kullanabilir.

  1. Izgara boyutunu öğrenmek için hesaplama gölgelendiricinizle birlikte bir üniforma kullanın. Örneğin:

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

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

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

}

Nokta Shader'ında olduğu gibi, hücre durumunu depolama arabelleği olarak da gösterirsiniz. Ancak bu durumda iki tane ihtiyacınız var. Hesaplama gölgelendiricilerinin, köşe konumu veya parça rengi gibi zorunlu bir çıkışı olmadığından, bir hesaplama gölgelendiricisinden sonuç elde etmenin 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 tamponunuz ve ızgaranın yeni durumunu yazdığınız bir tamponunuz vardır.

  1. Hücre giriş ve çıkış durumunu depolama alanı arabellekleri olarak gösterin. Örneğin:

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

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

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

}

İlk depolama arabelleğinin var<storage> ile tanımlandığını ve bu nedenle salt okunur olduğunu, ikinci depolama arabelleğinin ise var<storage, read_write> ile tanımlandığını unutmayın. Bu sayede, bu arabelleği hesaplama gölgelendiricinizin çıkışı olarak kullanarak arabelleğe hem okuma hem de yazma yapabilirsiniz. (WebGPU'de salt yazma depolama modu yoktur).

Ardından, hücre dizininizi doğrusal depolama dizisiyle eşleyebilmeniz gerekir. Bu, temel olarak köşe üstü gölgelendiricide yaptığınız işlemin tam tersidir. Köşe üstü gölgelendiricide doğrusal instance_index değerini alıp 2D ızgara hücresine eşlediniz. (Bu konudaki algoritmanız vec2f(i % grid.x, floor(i / grid.x)) idi.)

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

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

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

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

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

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

Son olarak, algoritmanın çalıştığını görmek için çok basit bir algoritma uygulayın: Bir hücre şu anda açıksa kapatılır ve bunun tersi de geçerlidir. Henüz Game of Life değil ancak hesaplama gölgelendiricisinin çalıştığını göstermek için yeterli.

  1. Basit algoritmayı şu şekilde ekleyin:

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

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

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

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

Hesaplama gölgelendiricinizle ilgili olarak şimdilik bu kadar. Ancak sonuçları görebilmeniz için birkaç değişiklik daha yapmanız gerekiyor.

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

Yukarıdaki gölgelendiricide, büyük ölçüde oluşturma ardışık düzeninizle aynı girişlerin (üniformalar ve depolama arabellekleri) kullanıldığını fark edebilirsiniz. Bu nedenle, aynı bağlama gruplarını kullanarak bu işi bitirebileceğinizi düşünebilirsiniz, değil mi? İyi haber şu ki bunu yapabilirsiniz. Bunu yapabilmek için biraz daha manuel kurulum yapmanız gerekir.

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

Bunun nedenini anlamak için şunu göz önünde bulundurun: Oluşturma ardışık düzenlerinizde tek bir tekdüzen arabellek ve tek bir depolama arabelleği kullanırsınız ancak yeni yazdığınız işleme gölgesinde ikinci bir depolama arabelleğine ihtiyacınız vardır. İki gölgelendirici, tekdüze ve ilk depolama tamponu için aynı @binding değerlerini kullandığından bunları ardışık düzenler arasında paylaşabilirsiniz. Oluşturma ardışık düzeni, kullanmadığı ikinci depolama tamponunu yoksayar. Yalnızca belirli bir ardışık düzen tarafından kullanılanları değil, bağlama grubunda bulunan tüm kaynakları açıklayan bir düzen oluşturmak istiyorsunuz.

  1. Bu düzeni oluşturmak için device.createBindGroupLayout() 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 listesi tanımladığınız için bağlama grubunun kendisine 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, kaynak için binding numarasını girersiniz. Bu numara, bağlayıcı grubunu oluşturduğunuzda öğ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üze hem de ilk depolama arabelleğine, köşe ve işleme gölgelendiricilerinde erişilebilmesini istersiniz ancak ikinci depolama arabelleğine yalnızca işleme gölgelendiricilerinde erişilebilmesi gerekir.

Son olarak, ne tür bir kaynak kullanıldığını belirtirsiniz. Bu, göstermeniz gereken öğeye bağlı olarak farklı bir sözlük anahtarıdır. Buradaki üç kaynağın tümü arabellek olduğundan, her biri için seçenekleri tanımlamak üzere buffer anahtarını kullanırsınız. texture veya sampler gibi diğer seçenekler de vardır ancak bunlara burada ihtiyacınız yoktur.

Arabellek sözlüğünde, arabellekteki type'ün ne kadarının kullanılacağı gibi seçenekleri belirlersiniz. Varsayılan değer "uniform" olduğundan, 0 bağlaması için sözlüğü boş bırakabilirsiniz. (Girişin arabellek olarak tanımlanması için en azından buffer: {} değerini ayarlamanız gerekir.) Bağlama 1, shader'da read_write erişimi ile kullanmadığınız için "read-only-storage" türüne sahiptir. Bağlama 2 ise read_write erişimi ile kullandığınız için "storage" türüne sahiptir.

bindGroupLayout oluşturulduktan sonra, ardışık düzenden bağlama grubunu sorgulamak yerine bağlama gruplarınızı oluştururken bunu iletebilirsiniz. Bu işlem, yeni tanımladığınız düzenle eşleşecek şekilde her bağlama grubuna yeni bir depolama alanı arabelleği girişi eklemeniz gerektiği anlamına gelir.

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

index.html

// Create a bind group to pass the grid uniforms into the pipeline
const bindGroups = [
  device.createBindGroup({
    label: "Cell renderer bind group A",
    layout: bindGroupLayout, // Updated Line
    entries: [{
      binding: 0,
      resource: { buffer: uniformBuffer }
    }, {
      binding: 1,
      resource: { buffer: cellStateStorage[0] }
    }, {
      binding: 2, // New Entry
      resource: { buffer: cellStateStorage[1] }
    }],
  }),
  device.createBindGroup({
    label: "Cell renderer bind group B",
    layout: bindGroupLayout, // Updated Line

    entries: [{
      binding: 0,
      resource: { buffer: uniformBuffer }
    }, {
      binding: 1,
      resource: { buffer: cellStateStorage[1] }
    }, {
      binding: 2, // New Entry
      resource: { buffer: cellStateStorage[0] }
    }],
  }),
];

Bağlama grubu bu açık bağlama grubu düzenini kullanacak şekilde güncellendiğine göre, oluşturma ardışık düzenini de aynı şekilde kullanacak şekilde güncellemeniz gerekir.

  1. Bir GPUPipelineLayout oluşturun.

index.html

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

Boru hattı düzeni, bir veya daha fazla boru hattının kullandığı bağlama grubu düzenlerinin listesidir (bu durumda bir tane vardır). Dizideki bağlama grubu düzenlerinin sırası, gölgelendiricilerdeki @group özelliklerine karşılık gelmelidir. (Bu, bindGroupLayout'ün @group(0) ile ilişkilendirildiği anlamına gelir.)

  1. Oluşturduğunuz ardışık düzeni, oluşturma ardışık düzenine "auto" yerine 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
    }]
  }
});

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

Nokta ve kırıntı gölgelendiricilerinizi kullanmak için bir oluşturma ardışık düzenine ihtiyacınız olduğu gibi, hesaplama gölgelendiricinizi kullanmak için de bir hesaplama ardışık düzenine ihtiyacınız vardır. Neyse ki işlem ardışık düzenleri, ayarlanacak herhangi bir duruma sahip olmadıkları ve yalnızca gölgelendirici ve düzeni içerdikleri için oluşturma ardışık düzenlerinden çok daha az karmaşıktır.

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

index.html

// Create a compute pipeline that updates the game state.
const simulationPipeline = device.createComputePipeline({
  label: "Simulation pipeline",
  layout: pipelineLayout,
  compute: {
    module: simulationShaderModule,
    entryPoint: "computeMain",
  }
});

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

Compute kartları

Bu noktada, işlem ardışık düzenini kullanmaya başlayabilirsiniz. Oluşturma işlemini bir oluşturma geçişinde yaptığınız için hesaplama işlemini de bir hesaplama geçişinde yapmanız gerektiğini tahmin edebilirsiniz. Hem hesaplama hem de oluşturma işlemi aynı komut kodlayıcısında 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 işlevle bir hesaplama geçişi başlatın (step++'den önce).

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

Hesaplama ardışık düzenlerinde olduğu gibi, hesaplama geçişlerinin başlatılması da çok daha basittir. Bunun nedeni, eklerle ilgili endişelenmenize gerek olmamasıdır.

Oluşturma işleminin, hesaplama işleminden gelen en son sonuçları hemen kullanmasına olanak tanıması nedeniyle hesaplama işlemini oluşturma işleminden önce yapmak istersiniz. Bu nedenle, geçişler arasında step sayısını artırarak hesaplama ardışık düzeninin çıkış arabelleğinin oluşturma ardışık düzeninin giriş arabelleği olmasını sağlarsınız.

  1. Ardından, oluşturma geçişinde, bağlama grupları arasında geçiş için oluşturma geçişinde kullandığınızla aynı kalıbı kullanarak ardışık düzeni ve bağlama grubunu ayarlayın.

index.html

const computePass = encoder.beginComputePass();

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

computePass.end();
  1. Son olarak, bir oluşturma geçişinde olduğu gibi çizim yapmak yerine, işi hesaplama gölgelendiricisine gönderirsiniz. Bu işlemde, 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 dikkat edilmesi gereken çok önemli bir nokta, dispatchWorkgroups() parametresine ilettiğiniz sayının çağrı sayısının olmadığıdır. Bunun yerine, yürütülecek iş gruplarının sayısıdır (shader'ınızdaki @workgroup_size tarafından tanımlanır).

Gölgelendiricinin, tüm ızgaranızı kapsayacak şekilde 32x32 kez yürütülmesini istiyorsanız ve iş grubunuzun boyutu 8x8 ise 4x4 iş grubu (4 * 8 = 32) dağıtmanız gerekir. Bu nedenle, ızgara boyutunu iş grubu boyutuna böler ve bu değeri dispatchWorkgroups() değişkenine iletirsiniz.

Artık sayfayı tekrar yenileyebilirsiniz. Her güncellemeyle birlikte ızgaranın tersine döndüğünü göreceksiniz.

Koyu mavi bir arka planda soldan sağa doğru giden renkli karelerden oluşan çapraz şeritler. Koyu mavi arka plan üzerinde, soldan sağa doğru iki kare genişliğinde renkli karelerden oluşan çapraz şeritler. Önceki görüntünün ters çevrimi.

Yaşam Oyunu algoritmasını uygulama

Nihai algoritmayı uygulamak için bilgi işlem gölgelendiriciyi güncellemeden önce, depolama alanı arabellek içeriğini başlatan koda geri dönüp her sayfa yüklendiğinde rastgele bir arabellek oluşturacak şekilde güncellemeniz gerekir. (Düzenli desenler, Yaşam Oyunu için çok ilginç başlangıç noktaları oluşturmaz.) Değerleri istediğiniz şekilde rastgele seçebilirsiniz ancak makul sonuçlar elde etmenizi sağlayacak kolay bir başlangıç yöntemi vardır.

  1. Her hücreyi rastgele bir durumda başlatmak için cellStateArray başlatma kodunu 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 Yaşam Oyunu simülasyonunun mantığını uygulayabilirsiniz. Bu noktaya gelene kadar yaptığınız her şeyin ardından gölgelendirici kodu sizi hayal kırıklığına uğratacak kadar basit olabilir.

Öncelikle, herhangi bir hücrenin komşularından kaçının etkin olduğunu bilmeniz gerekir. Hangilerinin etkin olduğunu değil, yalnızca sayıyı önemsiyorsunuz.

  1. Komşu hücre verilerini daha kolay elde etmek için belirli bir koordinatın cellStateIn değerini döndüren bir cellActive işlevi ekleyin.

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

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

cellActive işlevi, hücre etkinse bir değer döndürür. Bu nedenle, çevredeki sekiz hücrenin tümü için cellActive işlevinin döndürdüğü değeri topladığınızda kaç komşu hücrenin etkin olduğunu öğrenebilirsiniz.

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

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

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

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

Yaşam Oyunu'nda bu sorunu çözmenin yaygın ve kolay bir yolu, ızgaranın kenarındaki hücrelerin ızgaranın karşı ucundaki hücreleri komşuları olarak ele almasıdır. Bu sayede bir tür sarmalama efekti oluşturulur.

  1. cellIndex() işlevinde küçük bir değişiklik yaparak ızgara sarmalama özelliğini destekleyin.

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

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

X ve Y hücresini, ızgara boyutunun dışına çıktığında sarmalamak için % operatörünü kullanarak depolama alanı arabelleğinin sınırları dışında hiçbir zaman erişmediğinizden emin olabilirsiniz. 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 yitirir.
  • İki veya üç komşusu olan tüm 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 yitirir.

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

  1. Yaşam Oyunu mantığını aşağıdaki gibi uygulayın:

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

let i = cellIndex(cell.xy);

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

Referans olarak, nihai compute shader modülü çağrısı şu şekilde görünür:

index.html

const simulationShaderModule = device.createShaderModule({
  label: "Life simulation shader",
  code: `
    @group(0) @binding(0) var<uniform> grid: vec2f;

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

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

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

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

      let i = cellIndex(cell.xy);

      // Conway's game of life rules:
      switch activeNeighbors {
        case 2: {
          cellStateOut[i] = cellStateIn[i];
        }
        case 3: {
          cellStateOut[i] = 1;
        }
        default: {
          cellStateOut[i] = 0;
        }
      }
    }
  `
});

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

Koyu mavi arka plan üzerinde renkli hücrelerin oluşturulduğu, Yaşam Oyunu simülasyonundan bir örnek durumun ekran görüntüsü.

9. Tebrikler!

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

Yapabilecekleriniz

Daha fazla bilgi

Referans dokümanları