Bu codelab hakkında
1. Giriş
WebGPU nedir?
WebGPU, web uygulamalarında GPU'nuzun özelliklerine erişmek için yeni ve modern bir API'dir.
Modern API
WebGPU'dan önce, WebGPU özelliklerinin bir alt kümesini sunan WebGL vardı. Bu teknoloji, yeni bir sınıf zengin web içeriği oluşturma olanağı sağladı ve geliştiriciler bu teknolojiyle harika şeyler oluşturdu. Ancak 2007'de yayınlanan OpenGL ES 2.0 API'sini temel alıyordu. Bu API ise daha da eski bir OpenGL API'sini temel alıyordu. GPU'lar bu süre zarfında önemli ölçüde gelişti ve onlarla arayüz oluşturmak için kullanılan yerel API'ler de Direct3D 12, Metal ve Vulkan ile birlikte gelişti.
WebGPU, bu modern API'lerin sunduğu avantajları web platformuna getirir. GPU özelliklerini platformlar arası bir şekilde etkinleştirmeye odaklanırken web'de doğal hissettiren ve temel alındığı bazı yerel API'lerden daha az ayrıntılı bir API sunar.
Görüntü Oluşturma
GPU'lar genellikle hızlı ve ayrıntılı grafikler oluşturmayla ilişkilendirilir. WebGPU de buna istisna değildir. Hem masaüstü hem de mobil GPU'larda günümüzün en popüler oluşturma tekniklerinin çoğunu desteklemek için gereken özelliklere sahiptir ve donanım özellikleri gelişmeye devam ettikçe gelecekte yeni özelliklerin eklenmesine olanak tanır.
Bilgi işlem
WebGPU, oluşturmanın yanı sıra GPU'nuzun genel amaçlı, yüksek paralellikteki iş yüklerini gerçekleştirme potansiyelini de ortaya çıkarır. Bu hesaplama gölgelendiricileri, herhangi bir oluşturma bileşeni olmadan bağımsız olarak veya oluşturma ardışık düzeninizin sıkı bir şekilde entegre edilmiş bir parçası olarak kullanılabilir.
Bugünkü codelab'de, basit bir giriş projesi oluşturmak için WebGPU'nun hem oluşturma hem de hesaplama özelliklerinden nasıl yararlanacağınızı öğreneceksiniz.
Oluşturacağınız uygulama
Bu codelab'de, WebGPU'yu kullanarak Conway's Game of Life'i (Conway'in Yaşam Oyunu) oluşturacaksınız. Uygulamanız şunları yapabilecek:
- Basit 2D grafikler çizmek için WebGPU'nun oluşturma özelliklerini kullanın.
- Simülasyonu gerçekleştirmek için WebGPU'nun hesaplama özelliklerini kullanın.
Yaşam Oyunu, bir hücre ızgarasındaki hücrelerin belirli kurallara göre zaman içinde durumunu değiştirdiği hücresel otomat olarak bilinen bir oyundur. Yaşam Oyunu'nda hücreler, komşu hücrelerinin kaç tanesinin etkin olduğuna bağlı olarak etkin veya devre dışı hale gelir. Bu da izlerken dalgalanmalar gösteren ilginç desenlere yol açar.
Neler öğreneceksiniz?
- WebGPU'yu ayarlama ve tuval yapılandırması.
- Basit 2D geometri çizme.
- Çizilenleri değiştirmek için köşe ve parça gölgelendiricileri kullanma.
- Basit bir simülasyon gerçekleştirmek için hesaplama gölgelendiricileri kullanma.
Bu codelab, WebGPU'nun temel kavramlarını tanıtmaya odaklanır. Bu makale, API'nin kapsamlı bir incelemesi olarak tasarlanmamıştır ve 3D matris matematik gibi sıklıkla ilişkili konularla ilgili bilgileri kapsamaz (veya gerektirmez).
Gerekenler
- ChromeOS, macOS veya Windows'ta Chrome'un son sürümlerinden biri (113 veya sonraki sürümler) WebGPU, tarayıcı ve platformlar arası bir API'dir ancak henüz her yerde kullanıma sunulmamıştır.
- HTML, JavaScript ve Chrome Geliştirici Araçları hakkında bilgi sahibi olmanız gerekir.
WebGL, Metal, Vulkan veya Direct3D gibi diğer grafik API'lerini bilmeniz zorunludur. Ancak bu API'lerle ilgili deneyiminiz varsa WebGPU ile birçok benzerlik olduğunu fark edeceksiniz. Bu benzerlikler, öğrenme sürecinize hızlı bir başlangıç yapmanıza yardımcı olabilir.
2. Hazırlanın
Kodu alma
Bu kod laboratuvarının herhangi bir bağımlılığı yoktur ve WebGPU uygulamasını oluşturmak için gereken her adımda size yol gösterir. Bu nedenle, başlamak için herhangi bir kod yazmanıza gerek yoktur. Ancak https://glitch.com/edit/#!/your-first-webgpu-app adresinde kontrol noktası olarak kullanabileceğiniz bazı çalışan örnekler mevcuttur. Bu örneklere göz atabilir ve takıldığınızda referans olarak kullanabilirsiniz.
Geliştirici Konsolu'nu kullanın.
WebGPU, doğru kullanımı zorunlu kılan birçok kurala sahip oldukça karmaşık bir API'dir. Daha da kötüsü, API'nin işleyiş şekli nedeniyle birçok hata için tipik JavaScript istisnaları oluşturamaz. Bu da sorunun tam olarak nereden kaynaklandığını belirlemeyi zorlaştırır.
WebGPU ile geliştirirken, özellikle de yeni başlayan biriyseniz sorunlarla karşılaşacaksınız. Bu normaldir. API'nin arkasındaki geliştiriciler, GPU geliştirmeyle çalışmanın zorluklarının farkındadır ve WebGPU kodunuz bir hataya neden olduğunda geliştirici konsolunda sorunu tespit etmenize ve düzeltmenize yardımcı olacak çok ayrıntılı ve faydalı mesajlar alabilmeniz için çok çalıştı.
Herhangi bir web uygulaması üzerinde çalışırken konsolu açık tutmak her zaman faydalıdır ancak bu durum özellikle burada geçerlidir.
3. WebGPU'yu başlatma
<canvas>
ile başla
Tek amacınız hesaplama yapmaksa WebGPU'yu ekranda hiçbir şey göstermeden kullanabilirsiniz. Ancak kod laboratuvarımızda yapacağımız gibi bir şey oluşturmak istiyorsanız bir kanvas kullanmanız gerekir. Bu nedenle, başlangıç için iyi bir yer.
İçinde tek bir <canvas>
öğesi ve kanvas öğesini sorguladığımız bir <script>
etiketi bulunan yeni bir HTML dokümanı oluşturun. (Alternatif olarak Glitch'teki 00-starter-page.html dosyasını da kullanabilirsiniz.)
- Aşağıdaki kodu kullanarak bir
index.html
dosyası oluşturun:
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU Life</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
// Your WebGPU code will begin here!
</script>
</body>
</html>
Adaptör ve cihaz isteğinde bulunma
Artık WebGPU'ye geçebilirsiniz. Öncelikle, WebGPU gibi API'lerin tüm web ekosistemine yayılması biraz zaman alabileceğini göz önünde bulundurmalısınız. Bu nedenle, ilk önlem olarak kullanıcının tarayıcısının WebGPU'yu kullanıp kullanamayacağını kontrol etmek iyi bir fikirdir.
- WebGPU için giriş noktası görevi gören
navigator.gpu
nesnesinin olup olmadığını kontrol etmek üzere aşağıdaki kodu ekleyin:
index.html
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
İdeal olarak, sayfayı WebGPU'yu kullanmayan bir moda geçirerek WebGPU'nun kullanılamadığını kullanıcıya bildirmek istersiniz. (Bunun yerine WebGL kullanılabilir mi?) Ancak bu codelab'in amacı doğrultusunda, kodun daha fazla yürütülmesini durdurmak için bir hata atmanız yeterlidir.
WebGPU'nun tarayıcı tarafından desteklendiğini öğrendikten sonra, uygulamanız için WebGPU'yu başlatmanın ilk adımı bir GPUAdapter
istemektir. Adaptörü, WebGPU'nun cihazınızdaki belirli bir GPU donanım parçasının temsili olarak düşünebilirsiniz.
- Bağdaştırıcı almak için
navigator.gpu.requestAdapter()
yöntemini kullanın. Bir promise döndürür. Bu nedenle,await
ile çağırmak en uygun yöntemdir.
index.html
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No appropriate GPUAdapter found.");
}
Uygun bir adaptör bulunamazsa döndürülen adapter
değeri null
olabilir. Bu nedenle bu olasılığı ele almanız gerekir. Bu durum, kullanıcının tarayıcısı WebGPU'yu destekliyorsa ancak GPU donanımında WebGPU'yu kullanmak için gereken tüm özellikler yoksa ortaya çıkabilir.
Çoğu zaman, burada yaptığınız gibi tarayıcının varsayılan bir adaptör seçmesine izin vermeniz yeterlidir. Ancak daha gelişmiş ihtiyaçlar için requestAdapter()
'ye iletilen bağımsız değişkenler vardır. Bu bağımsız değişkenler, birden fazla GPU'ya sahip cihazlarda (bazı dizüstü bilgisayarlar gibi) düşük güçlü veya yüksek performanslı donanım kullanmak isteyip istemediğinizi belirtir.
Bir adaptörünüz olduğunda, GPU ile çalışmaya başlamadan önceki son adım GPUDevice istemek olur. Cihaz, GPU ile en fazla etkileşimin gerçekleştiği ana arayüzdür.
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 kanvastanGPUCanvasContext
isteyin. (Bu, sırasıyla2d
vewebgl
bağlam türlerini kullanarak Canvas 2D veya WebGL bağlamlarını başlatmak için kullanacağınız çağrıyla aynıdır.) Ardından, döndürülencontext
,configure()
yöntemi kullanılarak cihazla ilişkilendirilmelidir. Örneğin:
index.html
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
Burada iletilebilecek birkaç seçenek vardır ancak en önemlileri, bağlamı kullanacağınız device
ve bağlamın kullanması gereken doku biçimi olan format
'tır.
WebGPU'nun resim verilerini depolamak için kullandığı nesneler dokulardır. Her dokunun, GPU'nun bu verilerin bellekte nasıl düzenlendiğini bilmesini sağlayan bir biçimi vardır. Doku belleğinin işleyiş şekliyle ilgili ayrıntılar bu kod laboratuvarının kapsamı dışındadır. Önemli olan, kanvas bağlamının, kodunuzun çizebileceği dokular sağladığı ve kullandığınız biçimin, kanvasın bu resimleri ne kadar verimli bir şekilde gösterdiğini etkileyebileceğidir. Farklı cihaz türleri, farklı doku biçimleri kullanıldığında en iyi performansı gösterir. Cihazın tercih ettiği biçimi kullanmazsanız resim sayfanın bir parçası olarak gösterilmeden önce arka planda fazladan bellek kopyalarının yapılmasına neden olabilirsiniz.
Neyse ki WebGPU, tuvaliniz için hangi biçimi kullanacağınızı size söyler. Bu nedenle, bu konulardan endişelenmenize gerek yoktur. Neredeyse her durumda, yukarıda gösterildiği gibi navigator.gpu.getPreferredCanvasFormat()
çağrılarak döndürülen değeri iletmek istersiniz.
Tuvali temizleme
Bir cihazınız olduğuna ve kanvas bu cihazla yapılandırıldığına göre, kanvasın içeriğini değiştirmek için cihazı kullanmaya başlayabilirsiniz. Başlamak için düz bir renkle temizleyin.
Bunu yapmak (veya WebGPU'da hemen hemen her şeyi yapmak) için GPU'ya ne yapacağını bildiren bazı komutlar göndermeniz gerekir.
- Bunu yapmak için cihazın, GPU komutlarını kaydetmek için bir arayüz sağlayan bir
GPUCommandEncoder
oluşturmasını sağlayın.
index.html
const encoder = device.createCommandEncoder();
GPU'ya göndermek istediğiniz komutlar oluşturmayla (bu durumda, tuvali temizleme) ilgilidir. Bu nedenle, bir oluşturma geçişi başlatmak için encoder
komutunu kullanmanız gerekir.
Oluşturma geçişleri, WebGPU'daki tüm çizim işlemlerinin gerçekleştiği yerlerdir. Her biri, gerçekleştirilen tüm çizim komutlarının çıktısını alan dokuları tanımlayan bir beginRenderPass()
çağrısıyla başlar. Daha gelişmiş kullanımlar, oluşturulan geometrinin derinliğini depolamak veya kenar yumuşatma sağlamak gibi çeşitli amaçlara sahip ek olarak adlandırılan çeşitli dokular sağlayabilir. Ancak bu uygulama için yalnızca bir tane gereklidir.
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ğine sahip bir doku döndürür.
index.html
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
storeOp: "store",
}]
});
Doku, colorAttachment
öğesinin view
mülkü olarak verilir. Oluşturma geçişleri, dokudaki hangi kısımların oluşturulacağını belirten bir GPUTexture
yerine bir GPUTextureView
sağlamanız gerekir. Bu durum yalnızca daha gelişmiş kullanım alanları için önemlidir. Bu nedenle, burada createView()
işlevini doku üzerinde hiçbir bağımsız değişken olmadan çağırırsınız. Bu, oluşturma geçişinin dokudaki tüm alanı kullanmasını istediğinizi gösterir.
Ayrıca, oluşturma geçişinin başladığında ve sona erdiğinde doku üzerinde ne yapmasını istediğinizi de belirtmeniz gerekir:
loadOp
değeri"clear"
ise oluşturma geçişi başladığında dokunun temizlenmesini istediğinizi gösterir.storeOp
değeri"store"
olduğunda, oluşturma geçişi tamamlandıktan sonra oluşturma geçişi sırasında yapılan çizimlerin sonuçlarının dokuya kaydedilmesini istediğinizi belirtir.
Oluşturma işlemi başladıktan sonra hiçbir şey yapmanıza gerek yoktur. En azından şimdilik. Oluşturma geçişini loadOp: "clear"
ile başlatmak, doku görünümünü ve kanvası temizlemek için yeterlidir.
beginRenderPass()
'ten hemen sonra aşağıdaki çağrıyı ekleyerek oluşturma geçişini sonlandırın:
index.html
pass.end();
Bu çağrıların yapılmasının GPU'nun herhangi bir işlem yapmasına neden olmayacağını bilmek önemlidir. GPU'nun daha sonra yapması için komutları kaydediyorlar.
GPUCommandBuffer
oluşturmak için komut kodlayıcıdafinish()
işlevini çağırın. Komut arabelleği, kaydedilen komutların opak bir tutamacıdır.
index.html
const commandBuffer = encoder.finish();
GPUDevice
'unqueue
özelliğini kullanarak komut arabelleğini GPU'ya gönderin. Sıra, tüm GPU komutlarını yürüterek bunların iyi bir şekilde sıralandığından ve düzgün bir şekilde senkronize edildiğinden emin olur. Sıranınsubmit()
yöntemi bir komut arabelleği dizisi alır ancak bu durumda yalnızca bir tane vardır.
index.html
device.queue.submit([commandBuffer]);
Gönderdiğiniz bir komut arabelleği tekrar kullanılamaz. Bu nedenle, bu arabelleği saklamaya gerek yoktur. Daha fazla komut göndermek istiyorsanız başka bir komut arabelleği oluşturmanız gerekir. Bu nedenle, bu iki adımın tek bir adıma birleştirildiğini görmek oldukça yaygındır (bu kod laboratuvarının örnek sayfalarında olduğu gibi):
index.html
// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);
Komutları GPU'ya gönderdikten sonra JavaScript'in kontrolü tarayıcıya döndürmesine izin verin. Bu noktada tarayıcı, bağlamın mevcut dokusunu değiştirdiğinizi anlar ve kanvası bu dokuyu resim olarak gösterecek şekilde günceller. Bundan sonra tuval içeriğini tekrar güncellemek isterseniz yeni bir komut arabelleği kaydedip göndermeniz ve oluşturma geçişi için yeni bir doku almak üzere context.getCurrentTexture()
işlevini tekrar çağırmanız gerekir.
- 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.
Bir renk seçin.
Dürüst olmak gerekirse, siyah kareler oldukça sıkıcı. Bu nedenle, sonraki bölüme geçmeden önce biraz zaman ayırıp bu sayfayı kişiselleştirin.
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 mordur.{ r: 0, g: 0.3, b: 0, a: 1 }
koyu yeşildir.{ r: 0.5, g: 0.5, b: 0.5, a: 1 }
orta ton gridir.{ r: 0, g: 0, b: 0, a: 0 }
, varsayılan olarak saydam siyahtır.
Bu kod laboratuvarındaki örnek kod ve ekran görüntülerinde koyu mavi kullanılmıştır ancak istediğiniz rengi seçebilirsiniz.
- 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 çizecek: renkli bir kare. Bu kadar basit bir çıktı için çok fazla iş gibi göründüğünü belirtmek isteriz. Bunun nedeni, WebGPU'nun çok sayıda geometriyi çok verimli bir şekilde oluşturmak için tasarlanmış olmasıdır. Bu verimliliğin bir yan etkisi, nispeten basit şeyler yapmanın alışılmadık derecede zor görünmesidir. Ancak WebGPU gibi bir API'ye yöneliyorsanız biraz daha karmaşık bir şey yapmak istediğiniz için bu beklenti de vardır.
GPU'ların nasıl çizim yaptığını anlama
Daha fazla kod değişikliği yapmadan önce, GPU'ların ekranda gördüğünüz şekilleri nasıl oluşturduğuna dair çok hızlı, basitleştirilmiş ve üst düzey bir genel bakış yapmanız faydalı olacaktır. (GPU oluşturmanın işleyişiyle ilgili temel bilgilere aşinaysanız Köşe Noktalarını Tanımlama bölümüne atlayabilirsiniz.)
Kullanabileceğiniz çok sayıda şekil ve seçeneğe sahip Canvas 2D gibi bir API'nin aksine, GPU'nuz yalnızca birkaç farklı şekil türüyle (veya WebGPU tarafından adlandırıldığı şekliyle ilkel) çalışır: noktalar, çizgiler ve üçgenler. Bu codelab'de yalnızca üçgenler kullanacaksınız.
GPU'lar neredeyse yalnızca üçgenlerle çalışır. Bunun nedeni, üçgenlerin tahmin edilebilir ve verimli bir şekilde işlenmesini kolaylaştıran birçok güzel matematiksel özelliğe sahip olmasıdır. GPU ile çizdiğiniz neredeyse her şeyin, GPU'nun çizebilmesi için üçgene bölünmesi ve bu üçgenlerin köşe noktalarıyla tanımlanması gerekir.
Bu noktalar veya köşeler, WebGPU veya benzer API'ler tarafından tanımlanan bir kartezyen koordinat sisteminde bir noktayı tanımlayan X, Y ve (3D içerik için) Z değerleri olarak verilir. Koordinat sisteminin yapısını, sayfanızdaki tuvalle ilişkisi açısından düşünmek en kolayıdır. Kanvasınızın genişliği veya yüksekliği ne olursa olsun sol kenar X ekseninde her zaman -1, sağ kenar ise X ekseninde her zaman +1 olur. Benzer şekilde, alt kenar Y ekseninde her zaman -1, üst kenar ise Y ekseninde +1 olur. Yani (0, 0) her zaman kanvasın ortasında, (-1, -1) her zaman sol alt köşede ve (1, 1) her zaman sağ üst köşededir. Buna Klip Alanı denir.
Köşe noktaları başlangıçta bu koordinat sisteminde nadiren tanımlanır. Bu nedenle GPU'lar, köşe noktalarını klip alanına dönüştürmek için gereken tüm matematik işlemlerini ve köşe noktalarını çizmek için gereken diğer tüm hesaplamaları yapmak üzere köşe noktası gölgelendiricileri adlı küçük programlardan yararlanır. Örneğin, gölgelendirici bazı animasyonlar uygulayabilir veya köşeden ışık kaynağına olan yönü hesaplayabilir. Bu gölgelendiriciler, WebGPU geliştiricisi olarak siz tarafından yazılır ve GPU'nun işleyiş şekli üzerinde inanılmaz miktarda kontrol sağlar.
Ardından GPU, bu dönüştürülmüş köşelerden oluşan tüm üçgenleri alır ve bunları çizmek için ekrandaki hangi piksellere ihtiyaç duyulduğunu belirler. Ardından, her pikselin ne renk olması gerektiğini hesaplayan, yazdığınız parça gölgelendirici adlı başka bir küçük program çalıştırılır. Bu hesaplama, yeşil döndür kadar basit veya yüzeyin, yakındaki diğer yüzeylerden gelen güneş ışığına göre açısını hesaplama, sisle filtreleme ve yüzeyin ne kadar metalik olduğuna göre değiştirme kadar karmaşık olabilir. Tamamen sizin kontrolünüz altında olan bu işlem hem güçlendirici hem de bunaltıcı olabilir.
Bu piksel renklerinin sonuçları daha sonra bir dokuya toplanır ve ekranda gösterilebilir.
Köşeleri tanımlama
Daha önce de belirtildiği gibi, Yaşam Oyunu simülasyonu hücrelerden oluşan bir ızgara olarak gösterilir. Uygulamanızda, etkin hücreleri etkin olmayan hücrelerden ayırarak ızgaranın görselleştirilmesini sağlayan bir yöntem olmalıdır. Bu kod laboratuvarının yaklaşımı, etkin hücrelere renkli kareler çizip etkin olmayan hücreleri boş bırakmaktır.
Bu, GPU'ya karenin dört köşesinin her biri için birer farklı nokta sağlamanız gerektiği anlamına gelir. Örneğin, kanvasın ortasına çizilen ve kenarlardan biraz içeri çekilen bir karenin köşe koordinatları şu şekildedir:
Bu koordinatları GPU'ya beslemek için değerleri bir TypedArray içine yerleştirmeniz gerekir. Henüz aşina değilseniz TypedArrays, bitişik bellek blokları ayırmanıza ve serideki her öğeyi belirli bir veri türü olarak yorumlamanıza olanak tanıyan bir JavaScript nesne grubudur. Örneğin, bir Uint8Array
içinde dizideki her öğe tek bir imzasız bayttır. TypedArray'lar, WebAssembly, WebAudio ve (elbette) WebGPU gibi bellek düzenine duyarlı API'lerle veri alışverişinde bulunmak için mükemmeldir.
Kare örneğinde, değerler kesirli olduğu için Float32Array
uygundur.
- Aşağıdaki dizi tanımını kodunuza yerleştirerek diyagramdaki tüm köşe konumlarını içeren bir dizi oluşturun. Bu düğmeyi en üstte,
context.configure()
çağrısının hemen altında yerleştirebilirsiniz.
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8,
0.8, -0.8,
0.8, 0.8,
-0.8, 0.8,
]);
Boşluk ve yorumların değerler üzerinde hiçbir etkisi olmadığını unutmayın. Bunlar yalnızca size kolaylık sağlamak ve daha okunaklı hale getirmek içindir. Her değer çiftinin bir köşenin X ve Y koordinatlarını oluşturduğunu görmenize yardımcı olur.
Ancak bir sorun var. GPU'lar üçgenlerle çalışır. Yani köşeleri üçlü gruplar halinde sağlamanız gerekir. Dört kişilik bir grubunuz var. Çözüm, karenin ortasından bir kenar paylaşan iki üçgen oluşturmak için köşelerden ikisini tekrarlamaktır.
Şemadan kare oluşturmak için (-0,8; -0,8) ve (0,8; 0,8) köşe noktalarını mavi üçgen için bir kez, kırmızı üçgen için bir kez olmak üzere iki kez listelemeniz gerekir. (Köşeleri diğer iki köşeyle de bölmeyi seçebilirsiniz. Bu işlemde bir fark yoktur.)
- Önceki
vertices
dizinini şu şekilde güncelleyin:
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
Şemada, netlik için iki üçgen arasında bir ayrım gösterilmesine rağmen köşe konumları tamamen aynıdır ve GPU bunları boşluk olmadan oluşturur. Tek bir katı kare olarak oluşturulur.
Köşe ara belleği oluşturma
GPU, JavaScript dizisinden alınan verilerle köşe noktaları çizemez. GPU'ların genellikle oluşturma için yüksek oranda optimize edilmiş kendi bellekleri vardır. Bu nedenle, GPU'nun çizim yaparken kullanmasını istediğiniz tüm verilerin bu belleğe yerleştirilmesi gerekir.
Nokta verileri dahil olmak üzere birçok değer için GPU tarafındaki bellek GPUBuffer
nesneleri aracılığıyla yönetilir. Arabellek, GPU'nun kolayca erişebildiği ve belirli amaçlar için işaretlenmiş bir bellek bloğudur. Bunu GPU'da görünen bir TypedArray olarak düşünebilirsiniz.
- 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 dikkat etmeniz gereken nokta, arabelleğe bir etiket vermenizdir. Oluşturduğunuz her WebGPU nesnesine isteğe bağlı bir etiket verilebilir ve bunu kesinlikle yapmanız gerekir. Etiket, nesnenin ne olduğunu belirlemenize yardımcı olduğu sürece istediğiniz bir dize olabilir. Herhangi bir sorunla karşılaşırsanız bu etiketler, WebGPU'nun oluşturduğu hata mesajlarında neyin yanlış gittiğini anlamanıza yardımcı olmak için kullanılır.
Ardından, arabellek için bayt cinsinden bir boyut girin. 48 baytlık bir arabelleğe ihtiyacınız vardır. Bu arabelleği, 32 bitlik bir kayan noktanın boyutunu ( 4 bayt) vertices
dizinizdeki kayan nokta sayısıyla (12) çarparak belirlersiniz. Neyse ki TypedArray'lar byteLength değerlerini sizin için hesaplar. Bu nedenle, arabellek oluştururken bu değeri kullanabilirsiniz.
Son olarak, tamponun kullanım alanını belirtmeniz gerekir. Bu, GPUBufferUsage
işaretlerinden biri veya daha fazlasıdır. Birden fazla işaret, |
( bitsel VEYA) operatörüyle birleştirilir. Bu durumda, arabelleğin köşe noktası verileri (GPUBufferUsage.VERTEX
) için kullanılmasını ve ayrıca buraya veri kopyalayabilmeyi (GPUBufferUsage.COPY_DST
) istediğinizi belirtirsiniz.
Size döndürülen arabellek nesnesi opaktır. Bu nesnenin içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, özelliklerinin çoğu değiştirilemez. GPUBuffer
oluşturulduktan sonra yeniden boyutlandıramaz veya kullanım işaretlerini değiştiremezsiniz. Değiştirebileceğiniz tek şey hafızasındaki içeriklerdir.
Arabellek ilk oluşturulduğunda, içerdiği bellek sıfır olarak başlatılır. İçeriğini değiştirmenin birkaç yolu vardır ancak en kolayı, kopyalamak istediğiniz bir TypedArray ile device.queue.writeBuffer()
işlevini çağırmaktır.
- Köşe verilerini arabelleğin belleğine kopyalamak için aşağıdaki kodu ekleyin:
index.html
device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, vertices);
Köşe düzenini tanımlama
Artık içinde köşe verileri bulunan bir arabellek var ancak GPU açısından bu, yalnızca bir bayt kümesidir. Bu cihazla çizim yapacaksanız biraz daha bilgi vermeniz gerekiyor. WebGPU'ye, köşe verilerinin yapısı hakkında daha fazla bilgi vermeniz gerekir.
GPUVertexBufferLayout
sözlüğüyle köşe veri yapısını 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 noktası verisi türünü açıklayan GPUVertexFormat
türlerinin listesinden gelir. Köşelerinizde her biri 32 bitlik iki kayan nokta olduğundan float32x2
biçimini kullanırsınız. Bunun yerine köşe verileriniz her biri dört 16 bitlik işaretsiz tam sayıdan oluşuyorsa bunun yerine uint16x4
değerini kullanırsınız. Bu durumu fark ettiniz mi?
Ardından offset
, söz konusu özelliğin köşeden kaç bayt sonra başladığını belirtir. Bu konuda endişelenmeniz gereken tek durum, arabelleğinizde birden fazla özellik olmasıdır. Bu durum bu kod laboratuvarının konusu değildir.
Son olarak shaderLocation
'yi kullanabilirsiniz. Bu, 0 ile 15 arasında rastgele bir sayıdır ve tanımladığınız her özellik için benzersiz olmalıdır. Bu özelliği, bir sonraki bölümde bahsedeceğimiz köşe gölgelendiricisindeki belirli bir girişe bağlar.
Bu değerleri şimdi tanımlamış olsanız da henüz WebGPU API'ye aktarmadığınızı unutmayın. Bu konuyu daha sonra ele alacağız. Ancak bu değerleri, köşe noktalarınızı tanımladığınız noktada düşünmek en kolayıdır. Bu nedenle, bunları daha sonra kullanmak üzere şimdiden ayarlıyorsunuz.
Gölgelendiricilerle başlama
Oluşturmak istediğiniz verilere sahipsiniz ancak GPU'ya bu verileri tam olarak nasıl işleyeceğini söylemeniz gerekir. Bunun büyük bir kısmı gölgelendiricilerle gerçekleşir.
Gölgelendiriciler, yazdığınız ve GPU'nuzda çalıştırdığınız küçük programlardır. Her gölgelendirici, verilerin farklı bir aşamasında çalışır: Köşe işleme, Parçacık işleme veya genel Hesaplama. GPU'da olduklarından, ortalama JavaScript'inizden daha katı bir şekilde yapılandırılırlar. Ancak bu yapı, çok hızlı ve en önemlisi paralel olarak yürütmelerine olanak tanır.
WebGPU'daki gölgelendiriciler, WGSL (WebGPU Gölgelendirme Dili) adlı bir gölgelendirme dilinde yazılır. WGSL, söz dizimi açısından Rust'a biraz benzer. Sık kullanılan GPU çalışmalarını (ör. vektör ve matris matematikleri) daha kolay ve hızlı hale getirmeyi amaçlayan özelliklere sahiptir. Gölgelendirme dilinin tamamını öğretmek bu codelab'in kapsamının çok ötesindedir. Ancak basit örnekleri inceleyerek temel bilgilerden bazılarını öğrenebilirsiniz.
Gölgelendiriciler, WebGPU'ye dize olarak iletilir.
- Aşağıdaki kodu
vertexBufferLayout
altındaki kodunuza kopyalayarak gölgelendirici kodunuzu gireceğiniz bir yer oluşturun:
index.html
const cellShaderModule = device.createShaderModule({
label: "Cell shader",
code: `
// Your shader code will go here
`
});
device.createShaderModule()
olarak adlandırdığınız gölgelendiricileri oluşturmak için isteğe bağlı bir label
ve WGSL code
dizesi sağlarsınız. (Çok satırlık dizelere izin vermek için burada ters tırnak kullanmanız gerektiğini unutmayın.) Geçerli bir WGSL kodu eklediğinizde işlev, derlenmiş sonuçları içeren bir GPUShaderModule
nesnesi döndürür.
Köşe düğümü gölgelendiriciyi tanımlama
GPU da buradan başladığı için köşe üstü gölgelendirici ile başlayın.
Köşe üstü gölgelendirici bir işlev olarak tanımlanır ve GPU, vertexBuffer
'ünüzdeki her köşe için bu işlevi bir kez çağırır. vertexBuffer
'ünüz altı konuma (köşe) sahip olduğundan, tanımladığınız işlev altı kez çağrılır. Her çağrılışında, işleve vertexBuffer
'den farklı bir konum bağımsız değişken olarak iletilir ve köşe gölgelendirici işlevinin görevi, klip alanında ilgili konumu döndürmektir.
Bu kişilerin sırayla çağrılmayacağını da unutmayın. Bunun yerine GPU'lar, bu tür gölgelendiricileri paralel olarak çalıştırmada mükemmeldir ve aynı anda yüzlerce (hatta binlerce!) köşe işleme potansiyeline sahiptir. Bu, GPU'ların inanılmaz hızından sorumlu olan büyük bir faktördür ancak sınırlamaları vardır. Aşırı paralelleştirme sağlamak için köşe üstü gölgelendiriciler birbirleriyle iletişim kuramaz. Her gölgelendirici çağrısı, tek seferde yalnızca tek bir köşe noktasının verilerini görebilir ve yalnızca tek bir köşe noktasının değerlerini döndürebilir.
WGSL'de bir köşe üstü gölgelendirici işlevi istediğiniz şekilde adlandırılabilir ancak hangi gölgelendirici aşamasını temsil ettiğini belirtmek için önünde @vertex
özelliği olmalıdır. WGSL, işlevleri fn
anahtar kelimesiyle belirtir, bağımsız değişkenleri bildirmek için parantez kullanır ve kapsamı tanımlamak için köşeli parantez kullanır.
- Aşağıdaki gibi boş bir
@vertex
işlevi oluşturun:
index.html (createShaderModule kodu)
@vertex
fn vertexMain() {
}
Ancak bir köşe üstü gölgelendirici, işlenen köşe üstü için klip alanındaki nihai konumu en azından döndürmelidir. Bu değer her zaman 4 boyutlu bir vektör olarak verilir. Vektörler, gölgelendiricilerde yaygın olarak kullanıldığı için dilde birinci sınıf primitifler olarak kabul edilir ve 4 boyutlu bir vektör için vec4f
gibi kendi türlerine sahiptir. 2D vektörler (vec2f
) ve 3D vektörler (vec3f
) için de benzer türler vardır.
- Döndürülen değerin zorunlu konum olduğunu belirtmek için
@builtin(position)
özelliğiyle işaretleyin. İşlevin döndürdüğü değerin bu olduğunu belirtmek için->
simgesi kullanılır.
index.html (createShaderModule kodu)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
}
Elbette, işlevin bir dönüş türü varsa işlev gövdesinde gerçekten bir değer döndürmeniz gerekir. vec4f(x, y, z, w)
söz dizimini kullanarak döndürülecek yeni bir vec4f
oluşturabilirsiniz. x
, y
ve z
değerlerinin tümü, döndürülen değerde köşenin kırpma alanında nerede olduğunu belirten kayan nokta sayılarıdır.
- Statik bir
(0, 0, 0, 1)
değeri döndürdüğünüzde teknik olarak geçerli bir köşe üstü gölgelendiriciniz olur. Ancak GPU, oluşturduğu üçgenlerin tek bir nokta olduğunu algılayıp bunları atadığından bu gölgelendirici hiçbir zaman hiçbir şey göstermez.
index.html (createShaderModule kodu)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
return vec4f(0, 0, 0, 1); // (X, Y, Z, W)
}
Bunun yerine, oluşturduğunuz arabellekteki verilerden yararlanmak istiyorsunuz. Bunu, işleviniz için vertexBufferLayout
içinde tanımladığınızla eşleşen bir @location()
özelliği ve türü içeren bir bağımsız değişken açıklayarak yapabilirsiniz. 0
için bir shaderLocation
belirttiniz. Bu nedenle, WGSL kodunuzda bağımsız değişkeni @location(0)
ile işaretleyin. Ayrıca biçimi 2D vektör olan float32x2
olarak da tanımladınız. Bu nedenle, WGSL'de bağımsız değişkeniniz vec2f
olur. İstediğiniz adı verebilirsiniz ancak bunlar köşe konumlarınızı temsil ettiğinden pos gibi bir ad kullanmak doğal görünür.
- Gölgelendirici işlevinizi aşağıdaki kodla değiştirin:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
Şimdi bu konuma dönmeniz gerekiyor. Konum 2D vektör, döndürülen tür ise 4D vektör olduğundan bunu biraz değiştirmeniz gerekir. Yapmanız gereken, konum bağımsız değişkenindeki iki bileşeni alıp bunları döndürülen vektörün ilk iki bileşenine yerleştirmek ve son iki bileşeni sırasıyla 0
ve 1
olarak bırakmaktır.
- Hangi konum bileşenlerinin kullanılacağını açıkça belirterek doğru konumu döndürün:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos.x, pos.y, 0, 1);
}
Ancak bu tür eşlemeler gölgelendiricilerde çok yaygın olduğu için konum vektörünü, uygun bir kısaltma olarak ilk bağımsız değişken olarak da iletebilirsiniz. Bu, aynı anlama gelir.
return
ifadesini aşağıdaki kodla yeniden yazın:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
İlk tepe noktası gölgelendiriciniz hazır. Bu yöntem çok basittir. Konumun değiştirilmeden dağıtılmasından ibarettir ancak başlangıç için yeterlidir.
Parça gölgelendiriciyi tanımlama
Bir sonraki adım, parçacık gölgelendiricidir. Kırıntı gölgelendiricileri, köşe gölgelendiricilerine çok benzer şekilde çalışır ancak her köşe için çağrılmak yerine çizilen her piksel için çağrılır.
Kırıntı gölgelendiricileri her zaman köşe gölgelendiricilerinden sonra çağrılır. GPU, köşe düğümü gölgelendiricilerinin çıkışını alır ve üç nokta gruplarından üçgenler oluşturarak üçgenleştirir. Ardından, çıkış renk eklerinin hangi piksellerinin bu üçgene dahil olduğunu belirleyerek bu üçgenlerin her birini rasterize eder ve bu piksellerin her biri için bir kez parçacık gölgelendiriciyi çağırır. Kırıntı gölgelendirici, genellikle kendisine köşe gölgelendiricisinden gönderilen değerlerden ve GPU'nun renk eklemesine yazdığı dokular gibi öğelerden hesaplanan bir renk döndürür.
Düğüm gölgelendiricileri gibi, parçacık gölgelendiricileri de büyük ölçüde paralel bir şekilde yürütülür. Giriş ve çıkışları açısından köşe düğümü gölgelendiricilerinden biraz daha esnektirler ancak her üçgenin her pikseli için tek bir renk döndürdüklerini düşünebilirsiniz.
WGSL kırıntı gölgelendirici işlevi, @fragment
özelliğiyle gösterilir ve bir vec4f
döndürür. Ancak bu durumda vektör, konumu değil rengi temsil eder. Döndürülen rengin, beginRenderPass
çağrısındaki hangi colorAttachment
değerine yazıldığını belirtmek için dönüş değerine bir @location
özelliğinin eklenmesi gerekir. Yalnızca bir ek eklediğiniz için konum 0'dır.
- Aşağıdaki gibi boş bir
@fragment
işlevi oluşturun:
index.html (createShaderModule kodu)
@fragment
fn fragmentMain() -> @location(0) vec4f {
}
Döndürülen vektörün dört bileşeni kırmızı, yeşil, mavi ve alfa renk değerleridir. Bu değerler, daha önce beginRenderPass
içinde ayarladığınız clearValue
ile tam olarak aynı şekilde yorumlanır. vec4f(1, 0, 0, 1)
parlak kırmızıdır. Bu, kareniz için iyi bir renktir. Yine de istediğiniz renge ayarlayabilirsiniz.
- Döndürülen renk vektörünü şu şekilde ayarlayın:
index.html (createShaderModule kodu)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}
İşte tam bir kırıntı gölgelendirici. Çok ilginç bir kod değil. Her üçgenin her pikseli kırmızıya ayarlanıyor. Ancak şimdilik bu yeterli.
Yukarıda ayrıntılı olarak açıklanan gölgelendirici kodunu ekledikten sonra createShaderModule
çağrınız şu şekilde görünür:
index.html
const cellShaderModule = device.createShaderModule({
label: 'Cell shader',
code: `
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
Oluşturma ardışık düzeni oluşturma
Gölgelendirici modülü, tek başına oluşturma için kullanılamaz. Bunun yerine, device.createRenderPipeline() çağrısı yapılarak oluşturulan bir GPURenderPipeline
parçası olarak kullanmanız gerekir. Oluşturma ardışık düzeni, hangi gölgelendiricilerin kullanılacağı, köşe çubuğundaki verilerin nasıl yorumlanacağı, hangi tür geometrinin oluşturulacağı (çizgiler, noktalar, üçgenler...) gibi konular da dahil olmak üzere geometrinin nasıl çizileceğini kontrol eder.
Oluşturma ardışık düzeni, API'nin tamamındaki en karmaşık nesnedir ancak endişelenmeyin. Ona iletebileceğiniz değerlerin çoğu isteğe bağlıdır ve başlamak için yalnızca birkaçını sağlamanız gerekir.
- Şuna benzer bir oluşturma ardışık düzeni oluşturun:
index.html
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: "auto",
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
Her ardışık düzenin, ardışık düzenin ihtiyaç duyduğu giriş türlerini (köşe çubuğu hariç) açıklayan bir layout
öğesine ihtiyacı vardır ancak böyle bir öğeniz yoktur. Neyse ki şimdilik "auto"
değerini iletebilirsiniz. Bu durumda ardışık düzenleyici, gölgelendiricilerden kendi düzenini oluşturur.
Ardından vertex
aşamasıyla ilgili ayrıntıları sağlamanız gerekir. module
, köşe üstü gölgelendiricinizi içeren GPUShaderModule'dir ve entryPoint
, gölgelendirici kodunda her köşe üstü çağrısı için çağrılan işlevin adını verir. (Tek bir gölgelendirici modülünde birden fazla @vertex
ve @fragment
işlevi olabilir.) Arabellekler, verilerinizin bu ardışık düzeni kullandığınız köşe çubuğu arabelleklerinde nasıl paketlendiğini açıklayan bir GPUVertexBufferLayout
nesnesi dizisidir. Neyse ki bunu daha önce vertexBufferLayout
'ünüzde tanımlamıştınız. Burada, bu bilgileri gönderirsiniz.
Son olarak fragment
aşamasıyla ilgili ayrıntılar yer alır. Buna bir gölgelendirici modülü ve tepe noktası aşaması gibi giriş noktası da dahildir. Son olarak, bu ardışık düzenin kullanıldığı targets
öğesini tanımlamanız gerekir. Bu, ardışık düzenin çıktı olarak verdiği renk ekleriyle ilgili ayrıntıları (ör. doku format
) içeren bir sözlük dizisidir. Bu ayrıntıların, bu ardışık düzenin kullanıldığı tüm oluşturma geçişlerinin colorAttachments
bölümünde verilen dokularla eşleşmesi gerekir. Oluşturma geçişiniz, tuval bağlamındaki dokuları kullanır ve biçimi için canvasFormat
'te kaydettiğiniz değeri kullanır. Bu nedenle, burada da aynı biçimi iletirsiniz.
Bu, oluşturma ardışık düzeni oluştururken belirtebileceğiniz seçeneklerin tamamına yakını olmasa da bu codelab'in ihtiyaçları için yeterlidir.
Kareyi çizme
Böylece karenizi çizmek için ihtiyacınız olan her şeye sahip oldunuz.
- 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, çizim için hangi ardışık düzenin kullanılacağını belirtmek üzere setPipeline()
simgesini kullanırsınız. Kullanılan gölgelendiriciler, köşe noktası verilerinin düzeni ve diğer ilgili durum verileri buna dahildir.
Ardından, kareniz için köşe noktalarını içeren arabellekle setVertexBuffer()
işlevini çağırırsınız. Bu arabellek, geçerli ardışık düzenin vertex.buffers
tanımındaki 0. öğeye karşılık geldiği için 0
ile çağırırsınız.
Son olarak, draw()
aramasını yaparsınız. Bu arama, önceki tüm kurulumlardan sonra garip bir şekilde basit görünür. İletmek için tek yapmanız gereken, oluşturması gereken köşe sayısıdır. Bu sayı, şu anda ayarlanmış köşe tamponlarından alınır ve şu anda ayarlanmış ardışık düzen ile yorumlanır. Bunu 6
olarak sabit kodlayabilirsiniz ancak köşeler dizisinden hesaplamak (köşe başına 12 kayan nokta / 2 koordinat == 6 köşe), kareyi örneğin bir daireyle değiştirmeye karar verirseniz manuel olarak güncellemeniz gereken öğe sayısını azaltır.
- Ekranınızı yenileyin ve tüm emeklerinizin karşılığını nihayet görün: büyük bir renkli kare.
5. Izgara çizme
Öncelikle kendinizi tebrik edin. Geometrinin ilk parçalarını ekrana yansıtmak, çoğu GPU API'sinde genellikle en zor adımlardan biridir. Buradan yapacağınız tüm işlemler daha küçük adımlarla yapılabilir. Bu sayede, ilerlemenizi daha kolay doğrulayabilirsiniz.
Bu bölümde şunları öğreneceksiniz:
- JavaScript'ten gölgelendiriciye değişkenler (uniform olarak adlandırılır) nasıl aktarılır?
- Oluşturma davranışını değiştirmek için tek tipleri kullanma.
- Aynı geometrinin birçok farklı varyantını çizmek için örneklemeyi kullanma.
Izgara tanımlama
Bir ızgara oluşturmak için ızgara hakkında çok temel bir bilgiyi bilmeniz gerekir. Hem genişlik hem de yükseklik olarak kaç hücre içeriyor? Bu, geliştirici olarak size bağlıdır ancak işleri biraz daha kolaylaştırmak için ızgarayı kare (aynı genişlik ve yükseklik) olarak değerlendirin ve ikinin kuvveti olan bir boyut kullanın. (Bu, daha sonra bazı matematik işlemlerini kolaylaştırır.) Sonunda daha büyük bir ızgara oluşturmak isteyeceksiniz ancak bu bölümde kullanılan bazı matematik işlemlerini daha kolay göstermek için bu bölümün geri kalanında ızgara boyutunu 4x4 olarak ayarlayın. Daha sonra ölçeği artırın.
- JavaScript kodunuzun üst kısmına bir sabit ekleyerek ızgara boyutunu tanımlayın.
index.html
const GRID_SIZE = 4;
Ardından, karenizi GRID_SIZE
x GRID_SIZE
boyutunda tuvale sığdırabilmek için karenizi oluşturma şeklinizi güncellemeniz gerekir. Yani karenin çok daha küçük olması ve çok sayıda karenin olması gerekir.
Bu soruna yaklaşabileceğiniz bir yöntem, köşe ara belleğinizi önemli ölçüde büyütmek ve içinde GRID_SIZE
x GRID_SIZE
kare tanımlamak, bunları doğru boyut ve konumda yerleştirmektir. Bunun için kod yazmak aslında çok da zor değil. Birkaç for döngüsü ve biraz matematik yeterlidir. Ancak bu, GPU'dan en iyi şekilde yararlanmamakta ve efekti elde etmek için gerekenden daha fazla bellek kullanmaktadır. Bu bölümde, GPU'ya daha uygun bir yaklaşım ele alınmaktadır.
Tekdüze bir arabellek oluşturma
Öncelikle, seçtiğiniz ızgara boyutunu gölgelendiriciye iletmeniz gerekir. Gölgelendirici, öğelerin nasıl gösterileceğini değiştirmek için bu boyutu kullanır. Boyutu gölgelendiriciye sabit kod olarak ekleyebilirsiniz. Ancak bu durumda, ızgara boyutunu değiştirmek istediğinizde gölgelendiriciyi ve oluşturma ardışık düzenini yeniden oluşturmanız gerekir. Bu da pahalıdır. Daha iyi bir yöntem, ızgara boyutunu gölgelendiriciye üniforma olarak sağlamaktır.
Daha önce, bir köşe düğümü gölgelendiricisinin her çağrılmasında köşe düğümü arabelleğinden farklı bir değerin iletildiğini öğrenmiştiniz. Tekdüzen, her çağrı için aynı olan bir arabellekteki değerdir. Bu özellikler, bir geometri parçası (ör. konumu), animasyon çerçevesinin tamamı (ör. mevcut saat) veya hatta uygulamanın tüm yaşam döngüsü (ör. kullanıcı tercihi) için ortak olan değerleri iletmek amacıyla kullanışlıdır.
- Aşağıdaki kodu ekleyerek tekdüze bir arabellek oluşturun:
index.html
// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]);
const uniformBuffer = device.createBuffer({
label: "Grid Uniforms",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
Bu kod, daha önce köşe ara belleği oluşturmak için kullandığınız koda neredeyse tamamen aynı olduğu için size çok tanıdık gelecektir. Bunun nedeni, üniformaların WebGPU API'ye, köşe noktalarının bulunduğu GPUBuffer nesneleri aracılığıyla iletilmesidir. Buradaki temel fark, bu seferki usage
değerinin GPUBufferUsage.VERTEX
yerine GPUBufferUsage.UNIFORM
içermesidir.
Gölgelendiricideki tek biçimlere erişim
- Aşağıdaki kodu ekleyerek bir üniforma tanımlayın:
index.html (createShaderModule çağrısı)
// At the top of the `code` string in the createShaderModule() call
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos / grid, 0, 1);
}
// ...fragmentMain is unchanged
Bu, gölgelendiricinizde grid
adlı bir üniforma tanımlar. Bu üniforma, üniforma arabelleğine kopyaladığınız dizi ile eşleşen 2D kayan vektördür. Ayrıca üniformanın @group(0)
ve @binding(0)
'te bağlandığını belirtir. Bu değerlerin ne anlama geldiğini birazdan öğreneceksiniz.
Ardından, gölgelendirici kodunun başka bir yerinde ızgara vektörünü istediğiniz gibi kullanabilirsiniz. Bu kodda, köşe konumunu ızgara vektörüne bölersiniz. pos
bir 2D vektör ve grid
bir 2D vektör olduğundan WGSL bileşen bazında bölme işlemi gerçekleştirir. Diğer bir deyişle, sonuç vec2f(pos.x / grid.x, pos.y / grid.y)
ile aynıdır.
Birçok oluşturma ve hesaplama tekniği bu tür vektör işlemlerini kullandığından GPU gölgelendiricilerinde bu tür işlemler çok yaygındır.
Bu durum sizin durumunuzda, (4 boyutunda bir ızgara kullandıysanız) oluşturacağınız karenin orijinal boyutunun dörtte biri olacağı anlamına gelir. Dört tanesini bir satıra veya sütuna sığdırmak istiyorsanız bu mükemmel bir seçenektir.
Bağlantı grubu oluşturma
Ancak uniform'u gölgelendiricide belirtmek, onu oluşturduğunuz arabelleğe bağlamaz. Bunun için bir bağlantı grubu oluşturup ayarlamanız gerekir.
Bağlama grubu, gölgelendiricinizin aynı anda erişebilmesini istediğiniz kaynaklardan oluşan bir koleksiyondur. Tekdüze arabelleğiniz gibi çeşitli arabellek türleri ve burada ele alınmayan ancak WebGPU oluşturma tekniklerinin ortak parçaları olan dokular ve örnekleyiciler gibi diğer kaynakları içerebilir.
- Tekdüzen arabellek ve oluşturma ardışık düzeni oluşturulduktan sonra aşağıdaki kodu ekleyerek tekdüzen arabelleğinizle bir bağlama grubu oluşturun:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}],
});
Artık standart olan label
'unuza ek olarak, bu bağlama grubunun hangi tür kaynakları içerdiğini açıklayan bir layout
'a da ihtiyacınız vardır. Bu konuyu ilerideki bir adımda daha ayrıntılı olarak inceleyeceksiniz ancak şu anda layout: "auto"
ile oluşturduğunuz için ardışık düzeninizin bağlama grubu düzenini sorabilirsiniz. Bu, ardışık düzenin, gölgelendirici kodunda beyan ettiğiniz bağlamalardan otomatik olarak bağlama grubu düzenleri oluşturmasına neden olur. Bu durumda, 0
'un gölgelendiriciye yazdığınız @group(0)
değerine karşılık geldiği getBindGroupLayout(0)
değerini alırsınız.
Düzeni belirttikten sonra bir entries
dizisi sağlarsınız. Her giriş, en az aşağıdaki değerleri içeren bir sözlüktür:
binding
, gölgelendiriciye girdiğiniz@binding()
değerine karşılık gelir. Bu 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 değiştirilemez bir tutma yeri olan bir GPUBindGroup
döndürür. Bir bağlama grubunun işaret ettiği kaynakları oluşturulduktan sonra değiştiremezsiniz ancak bu kaynakların içeriklerini değiştirebilirsiniz. Örneğin, tekdüze arabelleği yeni bir ızgara boyutu içerecek şekilde değiştirirseniz bu, bu bağlama grubunu kullanan gelecekteki çizim çağrılarına yansıtılır.
Bağlama grubunu bağlama
Bağlama grubu oluşturulduktan sonra, WebGPU'ye çizim yaparken bu grubu kullanmasını söylemeniz gerekir. Neyse ki bu işlem oldukça basit.
- Oluşturma geçişine geri dönün ve
draw()
yönteminden önce bu yeni satırı ekleyin:
index.html
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup); // New line!
pass.draw(vertices.length / 2);
İlk bağımsız değişken olarak iletilen 0
, gölgelendirici kodundaki @group(0)
değerine karşılık gelir. @group(0)
'un parçası olan her @binding
'nin bu bağlama grubundaki kaynakları kullandığını söylüyorsunuz.
Artık tekdüze arabellek, gölgelendiricinize açıktır.
- Sayfanızı yenileyin. Ardından aşağıdakine benzer bir ekran görürsünüz:
Yaşasın! Kareniz artık önceki boyutunun dörtte biri kadar. Bu çok fazla bir şey değil ancak üniformanızın gerçekten uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.
Gölgelendiricide geometriyi değiştirme
Şimdi gölgelendiricide ızgara boyutuna referans verebiliyorsunuz. Bu nedenle, oluşturmakta olduğunuz geometriyi istediğiniz ızgara desenine uyacak şekilde değiştirmek için bazı çalışmalar yapmaya başlayabilirsiniz. Bunu yapmak için tam olarak neyi başarmak istediğinizi düşünün.
Kanvasınızı kavramsal olarak ayrı hücrelere ayırmanız gerekir. Sağa doğru ilerledikçe X ekseninin, yukarı doğru ilerledikçe Y ekseninin arttığı kuralını korumak için ilk hücrenin kanvasın sol alt köşesinde olduğunu varsayalım. Bu işlem, ortasında mevcut kare geometriniz bulunan aşağıdaki gibi bir düzen oluşturur:
Göreviniz, gölgelendiricide hücre koordinatları verilen bu hücrelerden herhangi birinde kare geometrisini konumlandırmanıza olanak tanıyan bir yöntem bulmaktır.
Öncelikle, kareniz tuvalin ortasını çevreleyecek şekilde tanımlandığı için hücrelerden hiçbiriyle düzgün bir şekilde hizalanmadığını görebilirsiniz. Kare, içlerinde düzgün bir şekilde hizalanabilmesi için yarım hücre kaydırılmalıdır.
Bu sorunu düzeltmenin bir yolu, karenin köşe ara belleğini güncellemektir. Köşeleri, sol alt köşe (-0,8, -0,8) yerine örneğin (0,1, 0,1) olacak şekilde kaydırarak bu kareyi hücre sınırlarıyla daha iyi hizalanacak şekilde hareket ettirebilirsiniz. Ancak, tepe noktalarının gölgelendiricinizde nasıl işlendiği üzerinde tam kontrole sahip olduğunuzdan, gölgelendirici kodunu kullanarak bunları kolayca yerine yerleştirebilirsiniz.
- 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);
}
Sayfayı hemen yenilediğinizde 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.
- Ofsetinizi 2 ile çarpın:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Böylece tam olarak istediğinizi elde edersiniz.
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
Bir miktar matematikle kareyi istediğiniz yere yerleştirebildiğinize göre, sonraki adımda ızgaranın her hücresine bir kare oluşturmanız gerekir.
Bu soruna yaklaşmanın bir yolu, hücre koordinatlarını tekdüze bir arabelleğe yazmak, ardından draw işlevini ızgaradaki her kare için bir kez çağırarak her seferinde tekdüzeliği güncellemektir. Ancak GPU'nun her seferinde yeni koordinatın JavaScript tarafından yazılmasını beklemesi gerektiğinden bu işlem çok yavaş olur. GPU'dan iyi performans elde etmenin anahtarlarından biri, sistemin diğer kısımlarını beklerken harcadığı süreyi en aza indirmektir.
Bunun yerine, örnekleme adı verilen bir tekniği kullanabilirsiniz. Örnekleme, GPU'ya draw
çağrısı yaparak aynı geometrinin birden fazla kopyasını çizmesini söylemenin bir yoludur. Bu yöntem, her kopya için draw
çağrısı yapmaktan çok daha hızlıdır. Geometrinin her kopyasına örnek adı verilir.
- GPU'ya, karenizi ızgaranın tamamını dolduracak kadar örnekle istediğinizi bildirmek için mevcut çizim çağrınıza bir bağımsız değişken ekleyin:
index.html
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
Bu, sisteme karenizdeki altı (vertices.length / 2
) köşeyi 16 (GRID_SIZE * GRID_SIZE
) kez çizmesini istediğinizi bildirir. Ancak sayfayı yenilediğinizde aşağıdakileri görmeye devam edersiniz:
Neden? Bunun nedeni, bu karelerin 16'sını da aynı yere çizmenizdir. Gölgelendiricide, geometriyi örnek başına yeniden konumlandıran ek bir mantığa ihtiyacınız vardır.
Gölgelendiricide, pos
gibi köşe çubuğunuzdan gelen köşe özelliklerine ek olarak WGSL'nin yerleşik değerleri olarak bilinen değerlere de erişebilirsiniz. Bunlar WebGPU tarafından hesaplanan değerlerdir ve bu değerlerden biri instance_index
'tür. instance_index
, gölgelendirici mantığınızın bir parçası olarak kullanabileceğiniz 0
ile number of instances - 1
arasında değişen, işaretsiz bir 32 bitlik sayıdır. Değeri, aynı örneğin parçası olan ve işlenen her köşe için aynıdır. Bu, köşe çubuğundaki her konum için bir kez olmak üzere köşe gölgelendiricinizin 0
değerine sahip bir instance_index
ile altı kez çağrılacağı anlamına gelir. Ardından 1
için instance_index
ile altı kez daha, 2
için instance_index
ile altı kez daha ve bu şekilde devam edin.
Bunun nasıl çalıştığını görmek için instance_index
yerleşik işlevini gölgelendirici girişlerinize eklemeniz gerekir. Bunu, konumla aynı şekilde yapın ancak @location
özelliğiyle etiketlemek yerine @builtin(instance_index)
kullanın ve ardından bağımsız değişkeni istediğiniz şekilde adlandırın. (Örnek koda uyması için instance
olarak adlandırabilirsiniz.) Ardından, bunu gölgelendirici mantığının bir parçası olarak kullanın.
- Hücre koordinatları yerine
instance
kullanın:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance); // Save the instance_index as a float
let cell = vec2f(i, i); // Updated
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Sayfayı şimdi yenilediğinizde birden fazla kareniz olduğunu görebilirsiniz. Ancak bunların 16'sını da göremezsiniz.
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 modülü ve %
operatörüyle WGSL'de gerçekleştirebileceğiniz ızgara genişliğini istiyorsunuz. Ayrıca, her hücrenin Y değeri için instance_index
değerinin, kalan kesirli kısmı atlayarak ızgara genişliğine bölünmesini istiyorsunuz. Bunu WGSL'nin floor()
işleviyle yapabilirsiniz.
- 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, kod laboratuvarının geri kalanının temelini attığınız için kolayca bir sonraki bölüme geçebilirsiniz. Ancak aynı rengi paylaşan karelerden oluşan bir ızgara kullanışlı olsa da pek heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla işleri biraz daha parlak hale getirebilirsiniz.
Yapıları gölgelendiricilerde kullanma
Şimdiye kadar, köşe düğümü gölgelendiricisinden bir veri parçası aktardınız: dönüştürülmüş konum. Ancak aslında, köşe düğümü gölgelendiricisinden çok daha fazla veri döndürebilir ve ardından bunları parçacık gölgelendiricide kullanabilirsiniz.
Verileri köşe düğümü gölgelendiricisinden aktarmanın tek yolu döndürmektir. Bir konumu döndürmek için her zaman bir köşe düğümü gölgelendirici gerekir. Bu nedenle, konumla birlikte başka veriler döndürmek istiyorsanız bunları bir yapıya yerleştirmeniz gerekir. WGSL'deki yapılar, bir veya daha fazla adlandırılmış özellik içeren adlandırılmış nesne türleridir. Mülkler @builtin
ve @location
gibi özelliklerle de işaretlenebilir. Bunları herhangi bir işlevin dışında tanımlarsınız ve daha sonra gerektiğinde örneklerini işlevlere aktarabilirsiniz. Örneğin, mevcut köşe düğümü gölgelendiricinizi düşünün:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
- Aynı şeyi, işlev girişi ve çıkışı için yapıları kullanarak ifade edin:
index.html (createShaderModule çağrısı)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
return output;
}
Bunun için giriş konumuna ve örnek dizinlerine input
ile başvurmanız gerektiğini ve döndürdüğünüz yapının önce bir değişken olarak tanımlanması ve ayrı ayrı özelliklerinin ayarlanması gerektiğini unutmayın. Bu durumda, çok fazla fark yaratmaz ve aslında gölgelendirici işlevini biraz daha uzatır. Ancak gölgelendiricileriniz daha karmaşık hale geldikçe yapıları kullanmak, verilerinizi düzenlemenize yardımcı olacak mükemmel bir yol olabilir.
Köşe ve parça işlevleri arasında veri aktarma
@fragment
işlevinizi mümkün olduğunca basit tutmanız gerektiğini hatırlatmak isteriz:
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
Hiçbir giriş almıyorsunuz ve çıkış olarak tek renk (kırmızı) gönderiyorsunuz. Ancak gölgelendirici, renklendirdiği geometri hakkında daha fazla bilgi sahibi olsaydı bu ek verileri kullanarak işleri biraz daha ilgi çekici hale getirebilirdiniz. Örneğin, her karenin rengini hücre koordinatına göre değiştirmek isterseniz ne yapabilirsiniz? @vertex
aşaması hangi hücrenin oluşturulduğunu bilir. @fragment
aşamasına iletmeniz yeterlidir.
Verileri köşe ve parça aşamaları arasında iletmek için seçtiğiniz bir @location
ile çıkış yapısına dahil etmeniz gerekir. Hücre koordinatını iletmek istediğiniz için daha önceki VertexOutput
yapısını ekleyin ve döndürmeden önce @vertex
işlevinde ayarlayın.
- Köşe yayıcınızın döndürülen değerini aşağıdaki gibi değiştirin:
index.html (createShaderModule çağrısı)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) cell: vec2f, // New line!
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell; // New line!
return output;
}
@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 edebilirsiniz. Bunun nedeni, @fragment
işlevinden döndürdüğünüz renk değerlerinin her kanalın 0 ile 1 aralığında olmasını beklemesi ve bu aralığın dışındaki tüm değerlerin bu aralığa kaydırmasıdır. Öte yandan, hücre değerleriniz her eksen boyunca 0 ile 32 arasında değişir. Burada gördüğünüz şey, ilk satır ve sütunun hemen kırmızı veya yeşil renk kanalında 1 değerine ulaştığı ve bundan sonraki her hücrenin aynı değere sabitlendiğidir.
Renkler arasında daha yumuşak bir geçiş istiyorsanız her renk kanalı için kesirli bir değer döndürmeniz gerekir. İdeal olarak her eksen boyunca sıfırdan başlayıp bir ile biten bir değer döndürmeniz gerekir. Bu da grid
'e bölme işleminin tekrarlanması anlamına gelir.
- Parça gölgelendiriciyi şu şekilde değiştirin:
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.cell/grid, 0, 1);
}
Sayfayı yenilediğinizde, yeni kodun tüm ızgara boyunca çok daha güzel bir renk gradyanı oluşturduğunu görebilirsiniz.
Bu kesinlikle bir iyileştirme olsa da sol alt köşede ızgaranın siyahlaştığı talihsiz bir karanlık köşe var. Yaşam Oyunu simülasyonunu başlattığınızda, ızgaranın zor görülebilen bir bölümü, neler olduğunu gizleyecektir. Bu konuyu açıklığa kavuşturalım.
Neyse ki kullanabileceğiniz kullanılmayan bir renk kanalınız (mavi) var. İdeal olarak, diğer renklerin en koyu olduğu yerde mavi rengin en parlak olmasını, ardından diğer renklerin yoğunluğu arttıkça mavi rengin soluklaşmasını istiyorsunuz. Bunu yapmanın en kolay yolu, kanalın 1'den başlamasını sağlamak ve hücre değerlerinden birini çıkarmaktır. c.x
veya c.y
olabilir. İkisini de deneyin ve ardından tercih ettiğinizi seçin.
- 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 bazı durumlara bağlı olarak, ızgaradaki hangi hücrelerin oluşturulacağını kontrol etmeniz gerekir. Bu, son simülasyon için önemlidir.
Tek ihtiyacınız her hücre için bir açma/kapatma sinyalidir. Bu nedenle, neredeyse her değer türünü içeren büyük bir dizi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun, tekdüze tamponların başka bir kullanım alanı olduğunu düşünebilirsiniz. Bu yöntemi kullanabilir olsanız da tekdüze arabelleklerin boyutu sınırlı olduğundan, dinamik boyutlu dizileri destekleyemediğinden (dizi boyutunu gölgelendiricide belirtmeniz gerekir) ve hesaplama gölgelendiricileri tarafından yazılamadığından daha zordur. Yaşam Oyunu simülasyonunu GPU'da bir işleme gölgelendiricisinde yapmak istediğiniz için en sorunlu olan son öğedir.
Neyse ki bu sınırlamaların hiçbirine takılmayan başka bir arabellek seçeneği var.
Depolama alanı arabelleği oluşturma
Depolama arabellekleri, işleme gölgelendiricilerinde okunup yazılabilen ve köşe gölgelendiricilerinde okunabilen genel amaçlı arabelleklerdir. Bunlar çok büyük olabilir ve bir gölgelendiricide belirli bir boyut beyan etmelerine gerek yoktur. Bu da onları genel belleğe çok daha benzer hale getirir. Hücre durumunu depolamak için bunu kullanırsınız.
- Hücre durumunuz için bir depolama tamponu oluşturmak üzere, şu anda muhtemelen tanıdık görünmeye başlayan tampon oluşturma kodu snippet'ini kullanın:
index.html
// Create an array representing the active state of each cell.
const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE);
// Create a storage buffer to hold the cell state.
const cellStateStorage = device.createBuffer({
label: "Cell State",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
Tıpkı köşe ve tekdüze arabelleklerinizde olduğu gibi, device.createBuffer()
işlevini uygun boyutta çağırın ve bu kez GPUBufferUsage.STORAGE
işlevinin kullanımını belirttiğinizden emin olun.
Aynı boyuttaki TypedArray'ı değerlerle doldurup device.queue.writeBuffer()
'yi çağırarak arabelleği daha önce olduğu gibi doldurabilirsiniz. Arabelleğinizin ızgara üzerindeki etkisini görmek istediğiniz için önce arabelleği tahmin edilebilir bir öğeyle doldurun.
- Aşağıdaki kodla her üç hücreyi etkinleştirin:
index.html
// Mark every third cell of the grid as active.
for (let i = 0; i < cellStateArray.length; i += 3) {
cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage, 0, cellStateArray);
Gölgelendiricide depolama arabelleğini okuma
Ardından, ızgarayı oluşturmadan önce depolama arabelleğinin içeriğini inceleyecek şekilde gölgelendiricinizi güncelleyin. Bu, daha önce üniformaların eklenme şekline çok benziyor.
- Gölgelendiricinizi aşağıdaki kodla güncelleyin:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellState: array<u32>; // New!
Öncelikle, ızgara üniformasının hemen altına yerleştirilen bağlama noktasını ekleyin. @group
değerini grid
üniformasıyla aynı tutmak istiyorsunuz ancak @binding
sayısının farklı olması gerekiyor. Farklı türde bir arabellek yansıtmak için var
türü storage
'tür ve JavaScript'teki Uint32Array
ile eşleşecek şekilde cellState
için verdiğiniz tür tek bir vektör yerine bir u32
değeri dizisidir.
Ardından, @vertex
işlevinizin gövdesinde hücrenin durumunu sorgulayın. Durum, depolama arabelleğinde düz bir dizi halinde depolandığından, geçerli hücrenin değerini aramak için instance_index
değerini kullanabilirsiniz.
Durum etkin değilse bir hücreyi nasıl kapatırsınız? Diziden aldığınız etkin ve etkin olmayan durumlar 1 veya 0 olduğundan geometriyi etkin duruma göre ölçeklendirebilirsiniz. 1'e ölçeklendirmek geometriyi olduğu gibi bırakır, 0'a ölçeklendirmek ise geometriyi tek bir noktaya çökertir ve GPU bu noktayı atar.
- Pozisyonu hücrenin etkin durumuna göre ölçeklendirmek için gölgelendirici kodunuzu güncelleyin. WGSL'nin tür güvenliği şartlarını karşılamak için durum değeri bir
f32
değerine dönüştürülmelidir:
index.html
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) -> VertexOutput {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let state = f32(cellState[instance]); // New line!
let cellOffset = cell / grid * 2;
// New: Scale the position by the cell's active state.
let gridPos = (pos*state+1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell;
return output;
}
Depolama alanı arabelleğini bağlama grubuna ekleme
Hücre durumunun geçerli hale geldiğini görebilmeniz için depolama arabelleğini bir bağlama grubuna ekleyin. Tekdüzen arabellekle aynı @group
'nin parçası olduğu için JavaScript kodunda da aynı bağlama grubuna ekleyin.
- Depolama alanı tamponunu şu şekilde ekleyin:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
},
// New entry!
{
binding: 1,
resource: { buffer: cellStateStorage }
}],
});
Yeni girişin binding
değerinin, gölgelendiricideki ilgili değerin @binding()
değeriyle eşleştiğinden emin olun.
Bu işlemi tamamladıktan sonra, yenilemeniz ve ızgaradaki kalıbı görebilmeniz gerekir.
Ping-pong arabelleği düzenini kullanma
Oluşturduğunuz gibi çoğu simülasyon, genellikle durumlarının en az iki kopyasını kullanır. Simülasyonun her adımında, durumun bir kopyasından okuma yapar ve diğerine yazar. Ardından, bir sonraki adımda kartı çevirin ve daha önce yazdıkları yerden okuyun. Durumun en güncel sürümü her adımda durum kopyaları arasında ileri geri gidip geldiğinden bu duruma genellikle ping pong denir.
Bu neden gerekli? Basitleştirilmiş bir örneğe göz atın: Her adımda etkin blokları sağa bir hücre taşıdığınız çok basit bir simülasyon yazdığınızı hayal edin. İşlemlerin kolay anlaşılır olması için verilerinizi ve simülasyonunuzu JavaScript'te tanımlarsınız:
// Example simulation. Don't copy into the project!
const state = [1, 0, 0, 0, 0, 0, 0, 0];
function simulate() {
for (let i = 0; i < state.length-1; ++i) {
if (state[i] == 1) {
state[i] = 0;
state[i+1] = 1;
}
}
}
simulate(); // Run the simulation for one step.
Ancak bu kodu çalıştırırsanız etkin hücre tek adımda dizinin sonuna kadar hareket eder. Neden? Durumu yerinde güncellemeye devam ettiğiniz için etkin hücreyi sağa taşırsınız ve bir sonraki hücreye bakarsınız. Etkin. Tekrar sağa hareket ettirin. Verileri gözlemlediğiniz sırada değiştirmeniz sonuçları bozar.
Ping pong modelini kullanarak, her zaman simülasyonun bir sonraki adımını yalnızca son adımın sonuçlarını kullanarak gerçekleştirdiğinizden emin olursunuz.
// Example simulation. Don't copy into the project!
const stateA = [1, 0, 0, 0, 0, 0, 0, 0];
const stateB = [0, 0, 0, 0, 0, 0, 0, 0];
function simulate(inArray, outArray) {
outArray[0] = 0;
for (let i = 1; i < inArray.length; ++i) {
outArray[i] = inArray[i-1];
}
}
// Run the simulation for two step.
simulate(stateA, stateB);
simulate(stateB, stateA);
- İ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:
index.html
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}],
})
];
Oluşturma döngüsü oluşturma
Şimdiye kadar sayfa yenileme başına yalnızca bir çizim yaptınız ancak artık zaman içinde güncellenen verileri göstermek istiyorsunuz. Bunun için basit bir oluşturma döngüsü gerekir.
Oluşturma döngüsü, içeriğinizi belirli bir aralıklarla tuvale çizen, sonsuz şekilde tekrarlanan bir döngüdür. Sorunsuz animasyonlar oluşturmak isteyen birçok oyun ve diğer içerik, geri çağırma işlemlerini ekranın yenilendiği hızda (saniyede 60 kez) planlamak için requestAnimationFrame()
işlevini kullanır.
Bu uygulama da bunu kullanabilir ancak bu durumda, simülasyonun ne yaptığını daha kolay takip edebilmeniz için güncellemelerin daha uzun adımlarla yapılmasını istersiniz. Bunun yerine, simülasyonunuzun güncellenme hızını kontrol etmek için döngüyü kendiniz yönetin.
- Öncelikle, simülasyonumuzun güncelleneceği bir hız seçin (200 ms iyi bir değerdir ancak isterseniz daha yavaş veya daha hızlı gidebilirsiniz). Ardından, simülasyonun kaç adımını tamamladığınızı takip edin.
index.html
const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
- Ardından, oluşturma işlemi için şu anda kullandığınız tüm kodu yeni bir işleve taşıyın. Bu işlevi,
setInterval()
ile istediğiniz aralıkta tekrarlanacak şekilde planlayın. İşlevin adım sayısını da güncellediğinden emin olun ve iki bağlama grubundan hangisinin bağlanacağını seçmek için bunu kullanın.
index.html
// Move all of our rendering code into a function
function updateGrid() {
step++; // Increment the step count
// Start a render pass
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1.0 },
storeOp: "store",
}]
});
// Draw the grid.
pass.setPipeline(cellPipeline);
pass.setBindGroup(0, bindGroups[step % 2]); // Updated!
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
// End the render pass and submit the command buffer
pass.end();
device.queue.submit([encoder.finish()]);
}
// Schedule updateGrid() to run repeatedly
setInterval(updateGrid, UPDATE_INTERVAL);
Artık uygulamayı çalıştırdığınızda kanvasın, oluşturduğunuz iki durum arabelleğini göstermek için ileri geri döndüğünü görürsünüz.
Bu işlemle birlikte, oluşturma işleminin büyük bir kısmını tamamlamış olursunuz. Bir sonraki adımda oluşturacağınız Yaşam Oyunu simülasyonunun çıktısını görüntülemeye hazırsınız. Bu adımda, hesaplama gölgelendiricileri kullanmaya başlayacaksınız.
WebGPU'nun oluşturma özelliklerinde, burada keşfettiğiniz küçük dilimden çok daha fazlası vardır ancak geri kalanı bu kod laboratuvarının kapsamı dışındadır. Bu makale, WebGPU'nun oluşturma işleminin nasıl çalıştığına dair yeterli bilgi vermeyi ve 3D oluşturma gibi daha gelişmiş teknikleri daha kolay anlamanıza yardımcı olmayı umuyor.
8. Simülasyonu çalıştırma
Şimdi, bulmacanın son önemli parçasına geçelim: Game of Life simülasyonunu bir hesaplama gölgelendiricisinde gerçekleştirme.
Hesaplama gölgelendiricileri nihayet kullanıma sunuldu.
Bu codelab boyunca, işleme gölgelendiricileri hakkında soyut bir şekilde bilgi edindiniz. Peki bunlar tam olarak nedir?
İşlem gölgelendiricileri, GPU'da aşırı paralellikle çalışacak şekilde tasarlandıkları için köşe ve parçacık gölgelendiricilere benzer ancak diğer iki gölgelendirici aşamasının aksine belirli bir giriş ve çıkış grubuna sahip değildir. Verileri yalnızca seçtiğiniz kaynaklardan (ör. depolama alanı arabellekleri) okuyor ve yazıyorsunuz. Bu, her köşe noktası, örnek veya piksel için bir kez yürütmek yerine, istediğiniz gölgelendirici işlevinin kaç kez çağrılmasını istediğinizi belirtmeniz gerektiği anlamına gelir. Ardından, gölgelendiriciyi çalıştırdığınızda hangi çağrının işlendiği size bildirilir ve buradan hangi verilere erişeceğinize ve hangi işlemleri yapacağınıza karar verebilirsiniz.
Hesaplama gölgelendiricileri, tıpkı köşe ve parçacık gölgelendiricileri gibi bir gölgelendirici modülünde oluşturulmalıdır. Bu nedenle, başlamak için bu modülü kodunuza ekleyin. Tahmin edebileceğiniz gibi, uyguladığınız diğer gölgelendiricilerin yapısı göz önüne alındığında, hesaplama gölgelendiricinizin ana işlevinin @compute
özelliğiyle işaretlenmesi gerekir.
- 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) {
}
global_invocation_id
yerleşik işlevini iletin. Bu işlev, gölgelendirici çağrısı ızgarasında nerede olduğunuzu belirten, üç boyutlu, işaretsiz tam sayılardan oluşan bir vektördür. Bu gölgelendiriciyi, ızgaranızdaki her hücre için bir kez çalıştırırsınız. (0, 0, 0)
, (1, 0, 0)
, (1, 1, 0)
... (31, 31, 0)
'a kadar olan sayılar alırsınız. Bu sayede, bu sayıyı üzerinde işlem yapacağınız hücre dizini olarak kullanabilirsiniz.
İşlem gölgelendiricileri, tıpkı köşe ve parçacık gölgelendiricilerinde kullandığınız gibi üniformalar da kullanabilir.
- Izgara boyutunu öğrenmek için hesaplama gölgelendiricinizle birlikte bir üniforma kullanın. Örneğin:
index.html (Compute createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f; // New line
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
Nokta Shader'ında olduğu gibi, hücre durumunu depolama arabelleği olarak da gösterirsiniz. Ancak bu durumda iki tane ihtiyacınız var. Hesaplama gölgelendiricilerinin, köşe konumu veya parça rengi gibi zorunlu bir çıkışı olmadığından, bir hesaplama gölgelendiricisinden sonuç elde etmenin tek yolu değerleri bir depolama arabelleğine veya dokuya yazmaktır. Daha önce öğrendiğiniz ping-pong yöntemini kullanın. Izgaranın mevcut durumunu besleyen bir depolama tamponunuz ve ızgaranın yeni durumunu yazdığınız bir tamponunuz vardır.
- Hücre giriş ve çıkış durumunu depolama alanı arabellekleri olarak gösterin. Örneğin:
index.html (Compute createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
// New lines
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
İlk depolama arabelleğinin var<storage>
ile tanımlandığını ve bu nedenle salt okunur olduğunu, ikinci depolama arabelleğinin ise var<storage, read_write>
ile tanımlandığını unutmayın. Bu sayede, bu arabelleği hesaplama gölgelendiricinizin çıkışı olarak kullanarak arabelleğe hem okuma hem de yazma yapabilirsiniz. (WebGPU'de salt yazma depolama modu yoktur).
Ardından, hücre dizininizi doğrusal depolama dizisiyle eşleyebilmeniz gerekir. Bu, temel olarak köşe üstü gölgelendiricide yaptığınız işlemin tam tersidir. Köşe üstü gölgelendiricide doğrusal instance_index
değerini alıp 2D ızgara hücresine eşlediniz. (Bu konudaki algoritmanız vec2f(i % grid.x, floor(i / grid.x))
idi.)
- Diğer yönde ilerleyen bir işlev yazın. Hücrenin Y değerini alır, ızgara genişliğiyle çarpar ve ardından hücrenin X değerini ekler.
index.html (Compute createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
// New function
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
Son olarak, algoritmanın çalıştığını görmek için çok basit bir algoritma uygulayın: Bir hücre şu anda açıksa kapatılır ve bunun tersi de geçerlidir. Henüz Game of Life değil ancak hesaplama gölgelendiricisinin çalıştığını göstermek için yeterli.
- Basit algoritmayı şu şekilde ekleyin:
index.html (Compute createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// New lines. Flip the cell state every step.
if (cellStateIn[cellIndex(cell.xy)] == 1) {
cellStateOut[cellIndex(cell.xy)] = 0;
} else {
cellStateOut[cellIndex(cell.xy)] = 1;
}
}
Hesaplama gölgelendiricinizle ilgili olarak şimdilik bu kadar. Ancak sonuçları görebilmeniz için birkaç değişiklik daha yapmanız gerekiyor.
Bağlama Grubu ve Ardışık Düzen Düzenlerini Kullanma
Yukarıdaki gölgelendiricide, büyük ölçüde oluşturma ardışık düzeninizle aynı girişlerin (üniformalar ve depolama arabellekleri) kullanıldığını fark edebilirsiniz. Bu nedenle, aynı bağlama gruplarını kullanarak bu işi bitirebileceğinizi düşünebilirsiniz, değil mi? İyi haber şu ki bunu yapabilirsiniz. Bunu yapabilmek için biraz daha manuel kurulum yapmanız gerekir.
Her bağlama grubu oluşturduğunuzda bir GPUBindGroupLayout
sağlamanız gerekir. Daha önce bu düzeni, oluşturma ardışık düzeninde getBindGroupLayout()
'ü çağırarak elde ediyordunuz. Bu da, oluştururken layout: "auto"
sağladığınız için düzeni otomatik olarak oluşturuyordu. Bu yaklaşım, yalnızca tek bir ardışık düzen kullandığınızda işe yarar. Ancak, kaynak paylaşmak isteyen birden fazla ardışık düzeniniz varsa düzeni açıkça oluşturmanız ve ardından hem bağlama grubuna hem de ardışık düzenlere sağlamanız gerekir.
Bunun nedenini anlamak için şunu göz önünde bulundurun: Oluşturma ardışık düzenlerinizde tek bir tekdüzen arabellek ve tek bir depolama arabelleği kullanırsınız ancak yeni yazdığınız işleme gölgesinde ikinci bir depolama arabelleğine ihtiyacınız vardır. İki gölgelendirici, tekdüze ve ilk depolama tamponu için aynı @binding
değerlerini kullandığından bunları ardışık düzenler arasında paylaşabilirsiniz. Oluşturma ardışık düzeni, kullanmadığı ikinci depolama tamponunu yoksayar. Yalnızca belirli bir ardışık düzen tarafından kullanılanları değil, bağlama grubunda bulunan tüm kaynakları açıklayan bir düzen oluşturmak istiyorsunuz.
- Bu düzeni oluşturmak için
device.createBindGroupLayout()
işlevini çağırın:
index.html
// Create the bind group layout and pipeline layout.
const bindGroupLayout = device.createBindGroupLayout({
label: "Cell Bind Group Layout",
entries: [{
binding: 0,
// Add GPUShaderStage.FRAGMENT here if you are using the `grid` uniform in the fragment shader.
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: {} // Grid uniform buffer
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage"} // Cell state input buffer
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage"} // Cell state output buffer
}]
});
Bu, entries
listesi tanımladığınız için bağlama grubunun kendisine benzer bir yapıya sahiptir. Aradaki fark, kaynağın kendisini sağlamak yerine girişin ne tür bir kaynak olması gerektiğini ve nasıl kullanıldığını açıklamanızdır.
Her girişte, kaynak için binding
numarasını girersiniz. Bu numara, bağlayıcı grubunu oluşturduğunuzda öğrendiğiniz gibi, gölgelendiricilerdeki @binding
değeriyle eşleşir. Ayrıca, kaynağı hangi gölgelendirici aşamalarının kullanabileceğini belirten GPUShaderStage
işaretleri olan visibility
değerini de sağlarsınız. Hem tekdüze hem de ilk depolama arabelleğine, köşe ve işleme gölgelendiricilerinde erişilebilmesini istersiniz ancak ikinci depolama arabelleğine yalnızca işleme gölgelendiricilerinde erişilebilmesi gerekir.
Son olarak, ne tür bir kaynak kullanıldığını belirtirsiniz. Bu, göstermeniz gereken öğeye bağlı olarak farklı bir sözlük anahtarıdır. Buradaki üç kaynağın tümü arabellek olduğundan, her biri için seçenekleri tanımlamak üzere buffer
anahtarını kullanırsınız. texture
veya sampler
gibi diğer seçenekler de vardır ancak bunlara burada ihtiyacınız yoktur.
Arabellek sözlüğünde, arabellekteki type
'ün ne kadarının kullanılacağı gibi seçenekleri belirlersiniz. Varsayılan değer "uniform"
olduğundan, 0 bağlaması için sözlüğü boş bırakabilirsiniz. (Girişin arabellek olarak tanımlanması için en azından buffer: {}
değerini ayarlamanız gerekir.) Bağlama 1, shader'da read_write
erişimi ile kullanmadığınız için "read-only-storage"
türüne sahiptir. Bağlama 2 ise read_write
erişimi ile kullandığınız için "storage"
türüne sahiptir.
bindGroupLayout
oluşturulduktan sonra, ardışık düzenden bağlama grubunu sorgulamak yerine bağlama gruplarınızı oluştururken bunu iletebilirsiniz. Bu işlem, yeni tanımladığınız düzenle eşleşecek şekilde her bağlama grubuna yeni bir depolama alanı arabelleği girişi eklemeniz gerektiği anlamına gelir.
- Bağlama grubu oluşturma işlemini şu şekilde güncelleyin:
index.html
// Create a bind group to pass the grid uniforms into the pipeline
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[1] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[0] }
}],
}),
];
Bağlama grubu bu açık bağlama grubu düzenini kullanacak şekilde güncellendiğine göre, oluşturma ardışık düzenini de aynı şekilde kullanacak şekilde güncellemeniz gerekir.
- 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 herhangi bir duruma sahip olmadıkları ve yalnızca gölgelendirici ve düzeni içerdikleri için oluşturma ardışık düzenlerinden çok daha az karmaşıktır.
- Aşağıdaki kodla bir hesaplama ardışık düzeni oluşturun:
index.html
// Create a compute pipeline that updates the game state.
const simulationPipeline = device.createComputePipeline({
label: "Simulation pipeline",
layout: pipelineLayout,
compute: {
module: simulationShaderModule,
entryPoint: "computeMain",
}
});
Güncellenen oluşturma ardışık düzeninde olduğu gibi, "auto"
yerine yeni pipelineLayout
öğesini ilettiğinize dikkat edin. Bu, hem oluşturma ardışık düzeninizin hem de hesaplama ardışık düzeninizin aynı bağlama gruplarını kullanmasını sağlar.
Compute kartları
Bu noktada, işlem ardışık düzenini kullanmaya başlayabilirsiniz. Oluşturma işlemini bir oluşturma geçişinde yaptığınız için hesaplama işlemini de bir hesaplama geçişinde yapmanız gerektiğini tahmin edebilirsiniz. Hem hesaplama hem de oluşturma işlemi aynı komut kodlayıcısında gerçekleşebilir. Bu nedenle, updateGrid
işlevinizi biraz karıştırmanız gerekir.
- Kodlayıcı oluşturma işlemini işlevin en üstüne taşıyın ve ardından işlevle bir hesaplama geçişi başlatın (
step++
'den önce).
index.html
// In updateGrid()
// Move the encoder creation to the top of the function.
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
// Compute work will go here...
computePass.end();
// Existing lines
step++; // Increment the step count
// Start a render pass...
Hesaplama ardışık düzenlerinde olduğu gibi, hesaplama geçişlerinin başlatılması da çok daha basittir. Bunun nedeni, eklerle ilgili endişelenmenize gerek olmamasıdır.
Oluşturma işleminin, hesaplama işleminden gelen en son sonuçları hemen kullanmasına olanak tanıması nedeniyle hesaplama işlemini oluşturma işleminden önce yapmak istersiniz. Bu nedenle, geçişler arasında step
sayısını artırarak hesaplama ardışık düzeninin çıkış arabelleğinin oluşturma ardışık düzeninin giriş arabelleği olmasını sağlarsınız.
- 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, bir oluşturma geçişinde olduğu gibi çizim yapmak yerine, işi hesaplama gölgelendiricisine gönderirsiniz. Bu işlemde, her eksende kaç çalışma grubu yürütmek istediğinizi belirtirsiniz.
index.html
const computePass = encoder.beginComputePass();
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
// New lines
const workgroupCount = Math.ceil(GRID_SIZE / WORKGROUP_SIZE);
computePass.dispatchWorkgroups(workgroupCount, workgroupCount);
computePass.end();
Burada dikkat edilmesi gereken çok önemli bir nokta, dispatchWorkgroups()
parametresine ilettiğiniz sayının çağrı sayısının olmadığıdır. Bunun yerine, yürütülecek iş gruplarının sayısıdır (shader'ınızdaki @workgroup_size
tarafından tanımlanır).
Gölgelendiricinin, tüm ızgaranızı kapsayacak şekilde 32x32 kez yürütülmesini istiyorsanız ve iş grubunuzun boyutu 8x8 ise 4x4 iş grubu (4 * 8 = 32) dağıtmanız gerekir. Bu nedenle, ızgara boyutunu iş grubu boyutuna böler ve bu değeri dispatchWorkgroups()
değişkenine iletirsiniz.
Artık sayfayı tekrar yenileyebilirsiniz. Her güncellemeyle birlikte ızgaranın tersine döndüğünü göreceksiniz.
Yaşam Oyunu algoritmasını uygulama
Nihai algoritmayı uygulamak için bilgi işlem gölgelendiriciyi güncellemeden önce, depolama alanı arabellek içeriğini başlatan koda geri dönüp her sayfa yüklendiğinde rastgele bir arabellek oluşturacak şekilde güncellemeniz gerekir. (Düzenli desenler, Yaşam Oyunu için çok ilginç başlangıç noktaları oluşturmaz.) Değerleri istediğiniz şekilde rastgele seçebilirsiniz ancak makul sonuçlar elde etmenizi sağlayacak kolay bir başlangıç yöntemi vardır.
- Her hücreyi rastgele bir durumda başlatmak için
cellStateArray
başlatma kodunu aşağıdaki kodla güncelleyin:
index.html
// Set each cell to a random state, then copy the JavaScript array
// into the storage buffer.
for (let i = 0; i < cellStateArray.length; ++i) {
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
Artık Yaşam Oyunu simülasyonunun mantığını uygulayabilirsiniz. Bu noktaya gelene kadar yaptığınız her şeyin ardından gölgelendirici kodu sizi hayal kırıklığına uğratacak kadar basit olabilir.
Öncelikle, herhangi bir hücrenin komşularından kaçının etkin olduğunu bilmeniz gerekir. Hangilerinin etkin olduğunu değil, yalnızca sayıyı önemsiyorsunuz.
- 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şiklik yaparak ızgara sarmalama özelliğini destekleyin.
index.html (Compute createShaderModule çağrısı)
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
X ve Y hücresini, ızgara boyutunun dışına çıktığında sarmalamak için %
operatörünü kullanarak depolama alanı arabelleğinin sınırları dışında hiçbir zaman erişmediğinizden emin olabilirsiniz. Bu sayede, activeNeighbors
sayısının tahmin edilebilir olduğundan emin olabilirsiniz.
Ardından, dört kuraldan birini uygularsınız:
- İkiden az komşusu olan hücreler etkinliğini yitirir.
- İki veya üç komşusu olan tüm etkin hücreler etkin kalır.
- Tam olarak üç komşusu olan tüm etkin olmayan hücreler etkin hale gelir.
- Üçten fazla komşusu olan hücreler etkinliğini yitirir.
Bunu bir dizi if ifadesiyle yapabilirsiniz ancak WGSL, bu mantık için iyi bir seçim olan switch ifadelerini de destekler.
- Yaşam Oyunu mantığını aşağıdaki gibi uygulayın:
index.html (Compute createShaderModule çağrısı)
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: { // Active cells with 2 neighbors stay active.
cellStateOut[i] = cellStateIn[i];
}
case 3: { // Cells with 3 neighbors become or stay active.
cellStateOut[i] = 1;
}
default: { // Cells with < 2 or > 3 neighbors become inactive.
cellStateOut[i] = 0;
}
}
Referans olarak, nihai compute shader modülü çağrısı şu şekilde görünür:
index.html
const simulationShaderModule = device.createShaderModule({
label: "Life simulation shader",
code: `
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// Determine how many active neighbors this cell has.
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: {
cellStateOut[i] = cellStateIn[i];
}
case 3: {
cellStateOut[i] = 1;
}
default: {
cellStateOut[i] = 0;
}
}
}
`
});
Hepsi bu kadar. Hepsi bu kadar! Sayfanızı yenileyin ve yeni oluşturduğunuz hücresel otomatınızın büyümesini izleyin.
9. Tebrikler!
WebGPU API'yi kullanarak klasik Conway's Game of Life simülasyonunun tamamen GPU'nuzda çalışan bir sürümünü oluşturdunuz.
Yapabilecekleriniz
- WebGPU Örnekleri'ni inceleyin.
Daha fazla bilgi
- WebGPU: Tüm çekirdekler, tuvalin hiçbiri
- Ham WebGPU
- WebGPU'nun Temel Özellikleri
- WebGPU En İyi Uygulamaları