1. Giriş
WebGPU nedir?
WebGPU, web uygulamalarında GPU'nuzun özelliklerine erişmek için kullanılan yeni ve modern bir API'dir.
Modern API
WebGPU'den ö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 avantajlarını 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şturacaklarınız
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.
Hücresel otomat olarak bilinen Hayat Oyunu kavramı, hücrelerden oluşan bir ızgaranın zaman içinde bazı kurallara bağlı olarak durumun değiştiği bir yöntemdir. Yaşam Oyunu'nda hücreler, komşu hü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'de, WebGPU'nun arkasındaki temel kavramları tanıtacağız. 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 olmak.
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 kullanılabilecek bazı çalışan örnekleri 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 çalışma şekli nedeniyle birçok hata için tipik JavaScript istisnaları bildiremez ve bu da sorunun tam olarak nereden geldiğini tam olarak belirlemeyi zorlaştırır.
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 almanız 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şlat
<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şlamak için iyi bir yer.
Tek bir <canvas>
öğesi ve tuval öğesini sorguladığımız bir <script>
etiketi içeren yeni bir HTML dokümanı oluşturun. (Alternatif olarak, glitch'ten 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ının 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.
- WebGPU için giriş noktası görevi gören
navigator.gpu
nesnesinin olup olmadığını kontrol etmek üzere aşağıdaki kodu ekleyin:
dizin.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'yi kullanabilir miyiz?) Ancak bu codelab'in amacı doğrultusunda, kodun daha fazla yürütülmesini durdurmak için bir hata atmanız yeterlidir.
Tarayıcının WebGPU'yu desteklediğini öğrendikten sonra, uygulamanız için WebGPU'yu başlatmanın ilk adımı bir GPUAdapter
isteğinde bulunmaktır. Adaptörü, WebGPU'nun cihazınızdaki belirli bir GPU donanım parçasının temsili olarak düşünebilirsiniz.
- 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.
dizin.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.
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 birGPUCanvasContext
isteyin. (Bu, sırasıyla2d
vewebgl
içerik türlerini kullanarak Tuval 2D veya WebGL bağlamlarını başlatmak için kullanacağınız çağrıyla aynıdır.) Sonrasında döndürdüğücontext
, aşağıdaki gibiconfigure()
yöntemi kullanılarak cihazla ilişkilendirilmelidir:
index.html
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
Burada iletilebilecek birkaç seçenek vardır ancak en önemlileri, bağlamı kullanacağınız device
ve bağlamın kullanması gereken doku biçimi olan format
'dı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ısıyla 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.
- 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.
dizin.html
const encoder = device.createCommandEncoder();
GPU'ya göndermek istediğiniz komutlar oluşturmayla (bu durumda, tuvalin temizlenmesi) 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çlarla ek adı verilen çeşitli dokular sağlayabilir. Ancak bu uygulama için yalnızca bir tanesine ihtiyacınız vardır.
context.getCurrentTexture()
işlevini çağırarak daha önce oluşturduğunuz tuval bağlamından doku alın. Bu işlev, tuvalinwidth
veheight
özelliklerine vecontext.configure()
işlevini çağırdığınız sırada belirtilenformat
değerine uygun piksel genişliği ve yüksekliği olan 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, 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"
, oluşturma geçişi tamamlandıktan sonra, oluşturma geçişi sırasında yapılan tüm çizimlerin sonuçlarının dokuya kaydedilmesini istediğinizi belirtir.
Oluşturma işlemi başladıktan sonra hiçbir şey yapmazsınız. En azından şimdilik. Oluşturma geçişini loadOp: "clear"
ile başlatma işlemi, doku görünümünü ve tuvali temizlemek için yeterlidir.
- Oluşturma aktarımını,
beginRenderPass()
öğesinin hemen sonrasına aşağıdaki çağrıyı ekleyerek 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. Bunlar sadece daha sonra GPU'nun yapması için komutları kaydeder.
GPUCommandBuffer
oluşturmak için komut kodlayıcıdafinish()
'ı çağırın. Komut arabelleği, kaydedilen komutların opak bir tutamacıdır.
index.html
const commandBuffer = encoder.finish();
GPUDevice
öğesininqueue
öğesini 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ınsubmit()
yöntemi, bir komut arabelleği dizisi alır ancak bu durumda yalnızca bir arabellek vardır.
dizin.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 codelab'in örnek sayfalarında olduğu gibi bu iki adımın tek bir adım altında daraltılmış olarak görülmesi oldukça yaygın bir durumdur:
index.html
// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);
Komutları GPU'ya gönderdikten sonra JavaScript'in kontrolü tarayıcıya döndürmesine izin verin. Bu noktada tarayıcı, bağlamın 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.
- 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.
Renk seçin
Dürüst olmak gerekirse, siyah kareler oldukça sıkıcı. Bu yüzden, içeriği biraz kişiselleştirmek için bir sonraki bölüme geçmeden önce birkaç dakikanızı ayırın.
encoder.beginRenderPass()
çağrısında,colorAttachment
öğesineclearValue
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 mor renktedir.{ r: 0, g: 0.3, b: 0, a: 1 }
koyu yeşildir.{ r: 0.5, g: 0.5, b: 0.5, a: 1 }
orta gridir.{ r: 0, g: 0, b: 0, a: 0 }
varsayılan şeffaf siyah renktir.
Bu codelab'deki örnek kod ve ekran görüntülerinde koyu mavi kullanılır ancak dilediğiniz rengi seçebilirsiniz.
- Renginizi seçtikten sonra sayfayı yeniden yükleyin. Seçtiğiniz rengi tuvalde görürsünüz.
4. Geometri çizme
Bu bölümün sonunda uygulamanız tuvale basit bir geometri çizecektir: 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 çizimlerini anlama
Daha fazla kod değişikliği yapmadan önce, GPU'ların ekranda gördüğünüz şekilleri nasıl oluşturduğuna dair çok hızlı, basitleştirilmiş ve üst düzey bir genel bakış yapmanızı öneririz. (GPU oluşturmanın işleyişiyle ilgili temel bilgilere aşina iseniz Köşe Noktalarını Tanımlama bölümüne atlayabilirsiniz.)
Kullanabileceğiniz çok sayıda şekil ve seçenek bulunan Canvas 2D gibi API'lerden farklı olarak GPU'nuz yalnızca birkaç farklı şekille (veya WebGPU'da temel öğelerle) (noktalar, çizgiler ve üçgenler) çalışır. Bu codelab'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 tarafından çizilebilmesi 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 yoldur. 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. Diğer bir deyişle, (0, 0) her zaman tuvalin merkezi, (-1, -1) her zaman sol alt köşedir ve (1, 1) her zaman sağ üst köşedir. Buna Klip Alanı denir.
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 biraz animasyon uygulayabilir veya tepe noktasından bir ışık kaynağına doğru 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, dönüştürülen bu köşelerin oluşturduğu tüm üçgenleri alır ve bunları çizmek için ekranda hangi piksellerin gerektiğini 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 doku halinde toplanır ve bu doku, 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, tuvalin merkezinde çizilen ve kenarlardan belirli bir şekilde içe doğru çekilen bir karenin köşe koordinatları şu şekildedir:
Bu koordinatları GPU'ya aktarmak için değerleri bir TypedArray içine yerleştirmeniz gerekir. Henüz aşina değilseniz TypedArrays, bitişik bellek blokları 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.
- Aşağıdaki dizi bildirimini kodunuza yerleştirerek diyagramdaki köşe konumlarının tümünü barındıran bir dizi oluşturun. Bu düğmeyi en üstte,
context.configure()
çağrısının hemen altında yerleştirebilirsiniz.
dizin.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8,
0.8, -0.8,
0.8, 0.8,
-0.8, 0.8,
]);
Boşluk ve yorumun değerler üzerinde 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.
Ama bir sorun var. GPU'lar üçgenlerle çalışır. Yani köşeleri üçlü gruplar halinde sağlamanız gerekir. Dört kişilik bir grubun var. Çözüm, köşelerden ikisini tekrarlayarak karenin ortasından geçen bir kenar paylaşan iki üçgen oluşturmaktır.
Diyagramdan kare oluşturmak için (-0,8, -0,8) ve (0,8, 0,8) köşe noktalarını bir kez mavi üçgen, diğeri kırmızı üçgen için olmak üzere iki kez listelemeniz gerekir. (Kareyi bunun yerine diğer iki köşesiyle bölmeyi de seçebilirsiniz; sonuçta herhangi bir fark olmaz.)
- Önceki
vertices
dizinizi aşağıdaki gibi görünecek şekilde güncelleyin:
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
Şemada, netlik için iki üçgen arasında 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 tamponu oluşturma
GPU, JavaScript dizisinden alınan verilerle köşe noktaları çizemez. GPU'lar genellikle oluşturma için son derece optimize edilmiş kendi belleğe sahiptir. Bu nedenle, GPU'nun çizim sırasında kullanmasını istediğiniz verilerin bu belleğe yerleştirilmesi gerekir.
Köşe verileri de dahil olmak üzere birçok değer için GPU tarafı belleği, GPUBuffer
nesneleri üzerinden yönetilir. Arabellek, GPU'nun kolayca eriş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.
- Köşe noktalarınızı tutacak bir arabellek oluşturmak için
vertices
dizinizin tanımından sonradevice.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 olarak, arabelleğe bir etiket vermeniz gerekir. 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 belirtin. 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ğerleri kullanabilirsiniz.
Son olarak, arabelleğin kullanımını belirtmeniz gerekir. Bu, |
( bit tabanlı VEYA) operatörüyle birleştirilen birden fazla işaretin olduğu GPUBufferUsage
işaretlerinden biri veya daha fazlasıdır. Bu durumda, arabelleğin köşe verileri için kullanılmasını (GPUBufferUsage.VERTEX
) ve verileri de bu verilere kopyalayabilmek (GPUBufferUsage.COPY_DST
) istediğinizi belirtirsiniz.
Size döndürülen arabellek nesnesi opaktır. Bu nesnenin içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, özelliklerinin çoğu değiştirilemez. Oluşturulan bir GPUBuffer
'yi yeniden boyutlandıramaz veya kullanım işaretlerini değiştiremezsiniz. Değiştirebileceğiniz şey, belleğinin içeriğidir.
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.
- Köşe verilerini arabelleğin belleğine kopyalamak için aşağıdaki kodu ekleyin:
dizin.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 gerekir. WebGPU'ye, köşe verilerinin yapısı hakkında daha fazla bilgi vermeniz gerekir.
- Köşe veri yapısını
GPUVertexBufferLayout
sözlüğüyle tanımlayın:
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 verisi türünü açıklayan GPUVertexFormat
türlerinin listesinden gelir. Köşe noktalarınızın her biri iki adet 32 bitlik kayan noktaya sahip 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. Yalnızca arabelleğinizde, bu codelab'de karşınıza çıkacak birden fazla özellik varsa bu konuda endişelenmeniz gerekir.
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 öğreneceğiniz tepe noktası gölgelendiricisindeki belirli bir girişe bağlar.
Bu değerleri şimdi tanımlasanız da henüz WebGPU API'ye aktarmadığınızı unutmayın. Bu konuyu daha sonra ele alacağız ancak bu değerleri en kolay şekilde köşe noktalarınızı tanımladığınız sırada düşünebilirsiniz. Bu nedenle, bunları daha sonra kullanmak üzere şimdiden ayarlıyorsunuz.
Gölgelendiricilerle başlama
Oluşturmak istediğiniz veriler elinizde ancak GPU'ya bu verilerin tam olarak nasıl işleneceğini söylemeniz gerekiyor. Bunun büyük bir kısmı gölgelendiricilerle gerçekleşir.
Gölgelendiriciler, yazdığınız ve GPU'nuzda yürütülen küçük programlardır. Her gölgelendirici, verilerin farklı bir aşamasında çalışır: Köşe işleme, Parçacık işleme veya genel Hesaplama. GPU'da olduklarından, ortalama JavaScript'inizden daha katı bir şekilde yapılandırılırlar. Ama bu yapı, ekiplerin çok hızlı bir şekilde ve en önemlisi de buna paralel bir şekilde faaliyet göstermesini sağlıyor.
WebGPU'daki gölgelendiriciler, WGSL (WebGPU Gölgelendirme Dili) adlı bir gölgelendirme dilinde yazılır. WGSL, söz dizimsel olarak Rust'a benzer. Yaygın kullanılan GPU iş türlerini (vektör ve matris matematiği gibi) daha kolay ve hızlı hale getirmeyi amaçlayan özellikler içerir. Gölgelendirme dilinin tamamını öğretmek bu codelab'in kapsamının çok ötesindedir. Ancak basit örnekleri inceleyerek temel bilgilerden bazılarını öğrenebilirsiniz.
Gölgelendiriciler, WebGPU'ya 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
Köşe gölgelendiriciyle başlayın çünkü GPU da burası burada başlıyor.
Köşe birleştirme programları 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 üstü gölgelendirici işlevinin görevi, klip alanında karşılık gelen bir konum döndürmektir.
Bunların da sıralı olarak çağrılmayabileceğini bilmeniz önemlidir. Bunun yerine GPU'lar, bu tür gölgelendiricileri paralel olarak çalıştırmada mükemmeldir ve aynı anda yüzlerce (hatta binlerce!) köşe iş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ı paralellik sağlamak için, tepe gölgelendiricileri birbirleriyle iletişim kuramaz. Her bir gölgelendirici çağrısı, aynı anda yalnızca tek bir tepe noktasına ait verileri görebilir ve yalnızca tek bir köşe noktası için değer üretebilir.
WGSL'de bir 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.
- Aşağıdaki gibi boş bir
@vertex
işlevi oluşturun:
index.html (createShaderModule kodu)
@vertex
fn vertexMain() {
}
Ancak köşe gölgesindeki bir tepe noktasının en azından klip alanında işlenen tepe noktasının son konumunu döndürmesi gerektiğinden bu durum geçerli değildir. 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.
- Döndürülen değerin gerekli konum olduğunu belirtmek için bunu
@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ü kayan nokta sayılarıdır. Döndürülen değerde, tepe noktasının klip alanında nerede olduğunu belirtir.
- 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 istersiniz. Bunu, işleviniz için vertexBufferLayout
içinde açıklamanızla eşleşen tür ve @location()
özelliğine sahip bir bağımsız değişken tanımlayarak yapabilirsiniz. 0
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
. İstediğiniz adı verebilirsiniz ancak bunlar köşe konumlarınızı temsil ettiğinden pos gibi bir ad kullanmak doğaldır.
- Gölgelendirici işlevinizi aşağıdaki kodla değiştirin:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
Şimdi bu konumu döndürmeniz gerekiyor. Konum 2D vektör ve dönüş türü 4D vektör olduğundan, onu biraz değiştirmeniz gerekir. 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.
- Hangi konum bileşenlerinin kullanılacağını açıkça belirterek doğru konumu döndürün:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos.x, pos.y, 0, 1);
}
Bununla birlikte, bu tür eşlemeler gölgelendiricilerde çok yaygın olduğundan, konum vektörünü kullanışlı bir kısaltmayla ilk bağımsız değişken olarak da aktarabilirsiniz ve bu aynı anlama gelir.
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
Sırada parça gölgelendirici var. 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 ekiniz olduğu için konum 0'dır.
- 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 ürünü istediğiniz renge ayarlayabilirsiniz.
- Döndürülen renk vektörünü şu şekilde ayarlayın:
index.html (createShaderModule kodu)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}
Bu, tamamlanmış bir parçacık gölgelendiricidir. Ç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, tüm API'deki 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:
dizin.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üzen, 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
hesabınızda tanımlamıştınız. Burada devreye girersiniz.
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
içinde kaydettiğiniz değeri kullanır. Dolayısıyla burada da aynı biçimi iletirsiniz.
Bu, görüntü oluşturma ardışık düzeni oluştururken belirtebileceğiniz seçeneklerin tümüne yakın olmasa da bu codelab'in ihtiyaçları için yeterli olacaktır.
Karesi çizin
Artık karenizi çizmek için ihtiyacınız olan her şeye sahipsiniz.
- Kare çizmek için
encoder.beginRenderPass()
vepass.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 setPipeline()
ile çizim için hangi ardışık düzenin kullanılması gerektiğini belirtirsiniz. 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, mevcut ardışık düzenin vertex.buffers
tanımındaki 0. öğeye karşılık geldiği için bu öğeyi 0
ile çağırırsınız.
Son olarak, 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. Sadece 6
şeklinde sabit kodlayabilirsiniz, ancak köşe dizisinden (12 kayan nokta / köşe başına 2 koordinat == 6 köşe), kareyi örneğin bir daireyle değiştirmeye karar verdiyseniz, elle güncelleyeceğiniz daha az şey olacağı anlamına gelir.
- Ekranınızı yenileyin ve tüm emeklerinizin karşılığını nihayet görün: büyük bir renkli kare.
5. Izgara çizin
Öncelikle, kendinizi tebrik etmek için bir dakikanızı ayırın. Geometrinin ilk parçalarını ekrana yansıtmak, çoğu GPU API'sinde genellikle en zor adımlardan biridir. Buradan yapacağınız her işlem 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 üniforma kullanma.
- Aynı geometrinin birçok farklı varyantını çizmek için örneklemeyi kullanma.
Izgarayı tanımlama
Bir ızgara oluşturmak için ızgara hakkında çok temel bir bilgiye sahip olmanız 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. Bu nedenle, 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, doğru boyut ve konumda. Aslında bunun kodu çok da kötü olmaz. 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, daha GPU uyumlu bir yaklaşım anlatılmaktadır.
Tekdüze bir arabellek oluşturma
Öncelikle, seçtiğiniz ızgara boyutunu gölgelendiriciye iletmeniz gerekir. Gölgelendirici, öğelerin nasıl görüntüleneceğ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ı bir işlemdir. 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öşeleriyle aynı GPUBuffer nesneleri üzerinden aktarılmasıdır. Aradaki temel fark, usage
süresinin bu sefer GPUBufferUsage.VERTEX
yerine GPUBufferUsage.UNIFORM
içermesidir.
Gölgelendiricideki tek tiplere 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 nokta vektörüdür. Ayrıca formanın @group(0)
ve @binding(0)
için bağlı olduğunu da belirtir. Bu değerlerin ne anlama geldiğini birazdan öğreneceksiniz.
Ardından, gölgelendirici kodunun başka bir yerinde ızgara vektörünü istediğiniz gibi kullanabilirsiniz. Bu kodda, 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. Bunlardan dördünü bir satıra veya sütuna sığdırmak istiyorsanız bu mükemmel bir seçenektir.
Bağlama Grubu Oluşturma
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, getBindGroupLayout(0)
isteğinde bulunursunuz. Buradaki 0
, gölgelendiriciye yazdığınız @group(0)
öğesine karşılık gelir.
Düzeni belirttikten sonra bir entries
dizisi sağlarsınız. Her giriş, en az 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 durumda0
.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 sabit bir herkese açık kullanıcı adı olan GPUBindGroup
değerini döndürür. Oluşturulduktan sonra bir bağlama grubunun işaret ettiği kaynakları değiştiremezsiniz ancak bu kaynakların içeriğini 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
Artık bağlama grubu oluşturulduğuna göre, yine de WebGPU'ya çizim sırasında bunu kullanmasını söylemeniz gerekir. Neyse ki bu işlem oldukça basit.
- Oluşturma kartına geri dönün ve bu yeni satırı
draw()
yönteminden önce ekleyin:
dizin.html
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup); // New line!
pass.draw(vertices.length / 2);
İlk bağımsız değişken olarak iletilen 0
, gölgelendirici kodundaki @group(0)
öğesine karşılık gelir. @group(0)
'un parçası olan her @binding
'nin bu bağlama grubundaki kaynakları kullandığını söylüyorsunuz.
Artık tek tip arabellek, gölgelendiricinize gösterilir.
- Sayfanızı yenilediğinizde şuna benzer bir sayfa görürsünüz:
Yaşasın! Kareniz artık önceki boyutunun dörtte biri kadar. Bu, çok fazla bir şey olmasa da formanızın aslında uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.
Gölgelendiricide geometriyi değiştirme
Şimdi gölgelendiricide ızgara boyutuna referans verebilir durumdasınız. Bu nedenle, oluşturmakta olduğunuz geometriyi istediğiniz ızgara desenine uyacak şekilde değiştirmek için bazı çalışmalar yapmaya başlayabilirsiniz. Bunun için tam olarak neyi başarmak istediğinizi düşünün.
Kanvasınızı kavramsal olarak ayrı hücrelere ayırmanız gerekir. Siz sağa doğru hareket ettikçe X ekseninin, yukarı hareket ettikçe Y ekseninin de artma kuralını korumak için, ilk hücrenin tuvalin sol alt köşesinde yer aldığını varsayalım. Bu işlem, ortada mevcut kare geometriniz olacak şekilde aşağıdaki gibi bir düzen oluşturur:
Göreviniz, gölgelendiricide hücre koordinatlarını kullanarak kare geometrisini bu hücrelerden herhangi birine yerleştirmenizi sağlayan bir yöntem bulmaktır.
Öncelikle, karenizin tuvalin merkezini çevreleyecek şekilde tanımlandığından hiçbir hücreyle düzgün bir şekilde hizalanmadığını görebilirsiniz. Kare, içlerinde düzgün bir şekilde hizalanabilmesi için yarım hücre kaydırılmalıdır.
Bunu düzeltmenin bir yolu, karenin köşe tamponunu 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.
- 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.
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.
- 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.
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.
- Izgara konumlandırmanızı aşağıdaki gibi değiştirin:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1); // Cell(1,1) in the image above
let cellOffset = cell / grid; // Compute the offset to cell
let gridPos = (pos + 1) / grid - 1 + cellOffset; // Add it here!
return vec4f(gridPos, 0, 1);
}
Şimdi yenilerseniz aşağıdakileri görürsünüz:
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.
- Ofseti 2 ile çarpın. Örneğin:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Böylece tam olarak istediğinizi elde edersiniz.
Ekran görüntüsü şu şekilde görünür:
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
Artık biraz matematik kullanarak kareyi istediğiniz yere yerleştirebileceğinize göre sonraki adım, ızgaranın her bir hücresinde bir kare oluşturmaktır.
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. Bununla birlikte, GPU'nun her seferinde JavaScript tarafından yeni koordinatın 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ı bir örnek olarak adlandırılır.
- GPU'ya, karenizi ızgarayı dolduracak kadar örnek istediğinizi bildirmek için mevcut çizim çağrınıza bir bağımsız değişken ekleyin:
dizin.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:
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 düğümü gölgelendiricinizin, köşe düğümü arabelleğinizde her konum için bir kez olmak üzere 0
değerine sahip bir instance_index
ile altı kez çağrılacağı anlamına gelir. Ardından instance_index
1
ile altı kez daha, ardından instance_index
tutarında 2
ile altı kez daha.
Bunun nasıl çalıştığını görmek için 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.
- Hücre koordinatları yerine
instance
kullanın:
dizin.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ı göremezsiniz.
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:
Bu hesaplama oldukça basittir. Her hücrenin X değeri için instance_index
değerinin modulo 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, kesirli kalanlar atlanarak ızgara genişliğine bölünmesini istiyorsunuz. Bunu, WGSL'nin floor()
işleviyle yapabilirsiniz.
- 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.
- Artık çalışıyor. Geri dönüp ızgara boyutunu artırın.
index.html
const GRID_SIZE = 32;
İş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, kodlab'ın geri kalanı için temeli attığınız için kolayca bir sonraki bölüme geçebilirsiniz. Aynı rengi paylaşan karelerden oluşan ızgaradan oluşan ızgara düzeninden yararlanılabilse de hiç heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla 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.
Köşe gölgelendiricisinden veri iletmenin tek yolu, verinin döndürülmesidir. 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. Tesisler, @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 bir çı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.
- Köşe yayıcınızın döndürülen değerini şu şekilde değiştirin:
index.html (createShaderModule çağrısı)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) cell: vec2f, // New line!
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell; // New line!
return output;
}
@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);
}
- 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);
}
- 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:
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 ediyor olabilirsiniz. Bunun nedeni, @fragment
işlevinden döndürdüğünüz renk değerlerinin her bir kanalın 0 ila 1 aralığında olmasını beklemesi ve bu aralığın dışındaki tüm değerlerin buna sabitlenmesidir. Öte yandan, hücre değerleriniz her eksen boyunca 0 ile 32 arasında değişir. Burada, ilk satır ve sütunun hemen kırmızı veya yeşil renk kanalında 1 değerine ulaştığını ve bundan sonraki her hücrenin aynı değere sabitlendiğini görüyorsunuz.
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
'ye bölme işleminin tekrarlanması anlamına gelir.
- 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ı sağladığını görebilirsiniz.
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 start 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.
- 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.
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 bir duruma bağlı olarak, ızgaradaki hangi hücrelerin oluşturulacağını kontrol etmeniz gerekir. Bu, son simülasyon için önemlidir.
Tüm ihtiyacınız olan, her hücre için bir açma/kapatma sinyalidir. Bu nedenle, neredeyse her değer türünde geniş bir diziyi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun, 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 bu yöntem daha zordur. Yaşam Oyunu simülasyonunu GPU'da bir işleme gölgesinde 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.
- Hücre durumunuza ilişkin bir depolama arabelleği oluşturmak için, şimdiye kadar muhtemelen arabellek oluşturma kodunun tanıdık görünümlü bir snippet'e dönüşecek şeyi kullanın:
index.html
// Create an array representing the active state of each cell.
const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE);
// Create a storage buffer to hold the cell state.
const cellStateStorage = device.createBuffer({
label: "Cell State",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
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.
Arabelleği, aynı boyuttaki TypedArray'i değerlerle doldurup device.queue.writeBuffer()
yöntemini çağırarak önceki gibi doldurabilirsiniz. Arabelleğinizin ızgara üzerindeki etkisini görmek istediğiniz için önce arabelleğinizi tahmin edilebilir bir öğeyle doldurun.
- 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ğine bakmak için gölgelendiricinizi güncelleyin. Bu, daha önce üniformaların eklenme şekline çok benziyor.
- 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. var
türü, farklı arabellek türünü yansıtmak amacıyla storage
değeridir ve tek bir vektör yerine cellState
için sağladığınız tür, JavaScript'te Uint32Array
ile eşleştirmek üzere u32
değerlerinden oluşan bir dizidir.
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.
Bir hücrenin etkin olmadığı belirtilmişse hücreyi nasıl kapatırsınız? Diziden aldığınız etkin ve etkin olmayan durumlar 1 veya 0 olduğundan, geometriyi etkin duruma göre ölçeklendirebilirsiniz. Geometriyi 1 ölçekle ölçeklendirmek, geometriyi yalnız bırakır, 0'a kadar ölçeklendirme ise geometrinin tek bir nokta haline gelmesine neden olur ve daha sonra GPU silinir.
- 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 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. Tek tip arabellekle aynı @group
öğesinin parçası olduğundan, bunu JavaScript kodunda da aynı bağlama grubuna ekleyin.
- Depolama arabelleğini aşağıdaki gibi ekleyin:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
},
// New entry!
{
binding: 1,
resource: { buffer: cellStateStorage }
}],
});
Yeni giriş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.
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ını okur ve diğerine yazarlar. 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. İşlemleri kolayca anlaşılır hale getirmek 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 anda değiştirmeniz, sonuçları bozar.
Pinpon kalıbını kullanarak yalnızca son adımın sonuçlarını kullanarak simülasyonun bir sonraki adımını her zaman uygulamanızı sağlarsınız.
// Example simulation. Don't copy into the project!
const stateA = [1, 0, 0, 0, 0, 0, 0, 0];
const stateB = [0, 0, 0, 0, 0, 0, 0, 0];
function simulate(inArray, outArray) {
outArray[0] = 0;
for (let i = 1; i < inArray.length; ++i) {
outArray[i] = inArray[i-1];
}
}
// Run the simulation for two step.
simulate(stateA, stateB);
simulate(stateB, stateA);
- İ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,
})
];
- İ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);
- 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:
dizin.html
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}],
})
];
Oluşturma döngüsü ayarlama
Şu ana kadar sayfa yenileme başına yalnızca bir çizim işlemi gerçekleştirdiniz, ancak şimdi zaman içinde güncellenen verileri göstermek istiyorsunuz. Bunun için basit bir oluşturma döngüsü gerekir.
Oluşturma döngüsü, içeriğinizi belirli bir aralıklarla tuvale çizen, sonsuz şekilde yinelenen 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. Simülasyonunuzun güncelleme hızını kontrol edebilmek için döngüyü kendiniz yönetebilirsiniz.
- Öncelikle, simülasyonunuzun 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
- 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 hangi 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.
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ölgelendiricilerini kullanmaya başlayacaksınız.
WebGPU'nun oluşturma yeteneklerinde, burada keşfettiğiniz küçük parçadan çok daha fazlasının olduğu açıktır ancak geri kalanlar bu codelab'in kapsamı dışındadır. Bu makalenin, WebGPU'nun oluşturma işleminin nasıl çalıştığına dair yeterli bilgi verdiğini umuyoruz. Bu sayede 3D oluşturma gibi daha gelişmiş teknikleri daha kolay anlayabilirsiniz.
8. Simülasyonu çalıştırma
Şimdi, bulmacanın son önemli parçasına geçelim: Yaşam Oyunu 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, örnek veya piksel için bir kez yürütmek yerine, gölgelendirici işlevinin kaç çağrısı istediğinizi belirtmeniz gerektiği anlamına gelir. Ardından, gölgelendiriciyi çalıştırdığınızda hangi çağrının işlendiği size bildirilir ve hangi verilere erişeceğinize ve buradan hangi işlemleri yapacağınıza karar verebilirsiniz.
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.
- 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.
- İş 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.
- Ç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.
- 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) {
}
Gölgelendirici çağrıları ızgarasında nerede olduğunuzu size bildiren ve imzalanmamış tam sayıların üç boyutlu bir vektörü olan yerleşik global_invocation_id
öğesini geçersiniz. 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 değerlendirebilirsiniz.
İşlem gölgelendiricileri, tıpkı köşe ve parçacık gölgelendiricilerinde kullandığınız gibi üniformalar da kullanabilir.
- 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) {
}
Tıpkı köşe verisi gölgelendiricide olduğu gibi, hücre durumunu da depolama arabelleği olarak 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.
- 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).
Ayrıca, hücre dizininizi doğrusal depolama dizisiyle eşleştirebileceğiniz bir yönteme sahip olmanız gerekir. Bu, temel olarak köşe ü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. (Bunun için algoritmanızın vec2f(i % grid.x, floor(i / grid.x))
olduğunu hatırlatmak isteriz.)
- Diğer yönde giden bir fonksiyon yazın. Hücrenin Y değerini alır, ızgara genişliğiyle çarpar ve ardından hücrenin X değerini ekler.
index.html (Compute createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
// New function
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
Son olarak, 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.
- 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? Güzel bir haberimiz var. 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 hesaplama 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.
- Bu düzeni oluşturmak için
device.createBindGroupLayout()
işlevini çağırın:
dizin.html
// Create the bind group layout and pipeline layout.
const bindGroupLayout = device.createBindGroupLayout({
label: "Cell Bind Group Layout",
entries: [{
binding: 0,
// Add GPUShaderStage.FRAGMENT here if you are using the `grid` uniform in the fragment shader.
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: {} // Grid uniform buffer
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage"} // Cell state input buffer
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage"} // Cell state output buffer
}]
});
Bu, entries
listesini tanımladığınız için bağlama 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ğlama grubunu oluşturduğunuzda öğrendiğiniz gibi, gölgelendiricilerdeki @binding
değeriyle eşleşir. Ayrıca, hangi gölgelendirici aşamalarının kaynağı kullanabileceğini gösteren GPUShaderStage
işaretleri olan visibility
bilgisini de sağlarsınız. Hem 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 seçenekler diğer seçenekler arasındadı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, bağlama grubunu ardışık düzenden sorgulamak yerine bağlama gruplarınızı oluştururken aktarabilirsiniz. Bunu yapmanız, az önce tanımladığınız düzenle eşleşmesi için her bir bağlama grubuna yeni bir depolama arabellek girişi eklemeniz gerektiği anlamına gelir.
- Bağlama grubu oluşturma işlemini şu şekilde güncelleyin:
index.html
// Create a bind group to pass the grid uniforms into the pipeline
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[1] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[0] }
}],
}),
];
Bağlama grubu bu açık bağlama grubu düzenini kullanacak şekilde güncellendi. Artık aynı şeyi kullanmak için oluşturma ardışık düzenini güncellemeniz gerekir.
- 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.)
- 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 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.
İşlem 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.
- Kodlayıcı oluşturmayı işlevin en üstüne taşıyın ve ardından bununla birlikte (
step++
öncesinde) bir işlem kartı başlatın.
index.html
// In updateGrid()
// Move the encoder creation to the top of the function.
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
// Compute work will go here...
computePass.end();
// Existing lines
step++; // Increment the step count
// Start a render pass...
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. İşlem ardışık düzeninin çıkış arabelleğinin, oluşturma ardışık düzeninin giriş arabelleği hâline gelmesi için geçişler arasında step
sayısını artırmanızın nedeni de budur.
- 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();
- Son olarak, oluşturma geçişinde olduğu gibi çizim yapmak yerine, her eksende kaç çalışma grubu yürütmek istediğinizi söyleyerek işi hesaplama gölgelendiricisine gönderirsiniz.
index.html
const computePass = encoder.beginComputePass();
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
// New lines
const workgroupCount = Math.ceil(GRID_SIZE / WORKGROUP_SIZE);
computePass.dispatchWorkgroups(workgroupCount, workgroupCount);
computePass.end();
Burada çok önemli dikkat edilmesi gereken bir nokta da dispatchWorkgroups()
ilettiğiniz sayının çağrı sayısı olmadığıdır. Bunun yerine, 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 ızgarayı kaplayacak şekilde 32x32 kez yürütülmesini istiyorsanız ve çalışma grubunuzun boyutu 8x8 ise, 4x4 çalışma gruplarını dağıtmanız gerekir (4 * 8 = 32). Bu nedenle, ızgara boyutunu 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.
Hayat Oyunu algoritmasını uygulama
Son algoritmayı uygulamak üzere Compute gölgeleyiciyi güncellemeden önce, depolama arabelleği içeriğini başlatan koda geri dönmek ve her sayfa yüklemesinde rastgele bir arabellek oluşturacak şekilde bunu güncellemek istersiniz. (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.
- 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, belirli 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.
- Komşu hücre verilerini daha kolay elde etmek için, belirli bir koordinatın
cellStateIn
değerini döndüren bircellActive
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.
- 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.
cellIndex()
işlevinde küçük bir değişiklikle birlikte ızgaranın çevrelenmesi desteği.
index.html (Compute createShaderModule çağrısı)
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
X ve Y hü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 devre dışı kalır.
- İki veya üç komşusu olan tüm etkin hücreler etkin kalır.
- Tam olarak üç komşusu olan etkin olmayan hücreler etkin hale gelir.
- Üçten fazla komşusu olan hücreler devre dışı bırakılır.
Bunu bir dizi "if" ifadesiyle yapabilirsiniz ancak WGSL, bu mantığa uygun olan "geçiş" ifadelerini de destekler.
- 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 sisteminin büyümesini izleyin.
9. Tebrikler!
WebGPU API'yi kullanarak klasik Conway'in Hayat Oyunu simülasyonunun tamamen GPU'nuzda çalışan bir sürümünü oluşturdunuz.
Sırada ne var?
- WebGPU Örnekleri'ni inceleyin.
Daha fazla bilgi
- WebGPU: Tüm çekirdekler, tuvalin hiçbiri
- Ham WebGPU
- WebGPU'nun Temel Özellikleri
- WebGPU En İyi Uygulamaları