Bu codelab hakkında
1. Giriş
WebGPU nedir?
WebGPU, web uygulamalarında GPU'nuzun özelliklerine erişmek için kullanılan yeni ve modern bir API'dir.
Modern API
WebGPU'dan önce, WebGPU'nun özelliklerinin bir alt kümesini sunan WebGL vardı. Bu teknoloji, yeni bir zengin web içeriği sınıfının ortaya çıkmasını sağladı ve geliştiriciler bu teknolojiyle harika şeyler yarattı. Ancak bu API, 2007'de yayınlanan ve daha da eski OpenGL API'sine dayanan OpenGL ES 2.0 API'sine dayanıyordu. GPU'lar bu süre zarfında önemli ölçüde gelişti ve bunlarla arayüz oluşturmak için kullanılan yerel API'ler de Direct3D 12, Metal ve Vulkan ile birlikte gelişti.
WebGPU, bu modern API'lerin avantajlarını web platformuna getirir. Bu API, GPU özelliklerini platformlar arası bir şekilde etkinleştirmeye odaklanırken web'de doğal bir his veren ve üzerine kurulu olduğu bazı yerel API'lerden daha az ayrıntılı bir API sunar.
Görüntü Oluşturma
GPU'lar genellikle hızlı ve ayrıntılı grafikler oluşturmakla ilişkilendirilir. WebGPU da bu konuda farklı değildir. Hem masaüstü hem de mobil GPU'larda günümüzün en popüler oluşturma tekniklerinin çoğunu desteklemek için gereken özelliklere sahiptir ve donanım özellikleri gelişmeye devam ettikçe gelecekte yeni özelliklerin eklenmesine olanak tanır.
Bilgi işlem
WebGPU, oluşturmanın yanı sıra genel amaçlı ve yüksek düzeyde paralel iş yüklerini gerçekleştirmek için GPU'nuzun potansiyelini ortaya çıkarır. Bu hesaplama gölgelendiricileri, herhangi bir oluşturma bileşeni olmadan bağımsız olarak veya oluşturma ardışık düzeninizin sıkı bir şekilde entegre edilmiş parçası olarak kullanılabilir.
Bu codelab'de, basit bir giriş projesi oluşturmak için WebGPU'nun hem oluşturma hem de hesaplama özelliklerinden nasıl yararlanacağınızı öğreneceksiniz.
Ne oluşturacaksınız?
Bu codelab'de, WebGPU kullanarak Conway'in Yaşam Oyunu'nu oluşturacaksınız. Uygulamanız şunları yapabilecek:
- Basit 2D grafikler çizmek için WebGPU'nun oluşturma özelliklerini kullanın.
- Simülasyonu gerçekleştirmek için WebGPU'nun hesaplama özelliklerini kullanın.
Yaşam Oyunu, bir dizi kurala göre zaman içinde durum değiştiren bir hücre ızgarasının bulunduğu hücresel otomat olarak bilinir. Yaşam Oyunu'nda hücreler, komşu hücrelerinden kaçının etkin olduğuna bağlı olarak etkin veya devre dışı hale gelir. Bu da izlerken değişen ilginç desenlere yol açar.
Neler öğreneceksiniz?
- WebGPU'yu ayarlama ve tuvali yapılandırma
- Basit 2D geometri nasıl çizilir?
- Çizilenleri değiştirmek için köşe ve parça gölgelendiricileri kullanma
- Basit bir simülasyon gerçekleştirmek için hesaplama gölgelendiricilerini kullanma.
Bu codelab, WebGPU'nun temel kavramlarını tanıtmaya odaklanmaktadır. Bu doküman, API'nin kapsamlı bir incelemesi olarak tasarlanmamıştır ve 3D matris matematiği gibi sıklıkla ilgili olan konuları kapsamaz (veya gerektirmez).
Gerekenler
- ChromeOS, macOS veya Windows'da Chrome'un yeni bir sürümü (113 ya da sonraki sürümler). WebGPU, tarayıcılar ve platformlar arası bir API'dir ancak henüz her yerde kullanıma sunulmamıştır.
- HTML, JavaScript ve Chrome Geliştirici Araçları hakkında bilgi sahibi olmak
WebGL, Metal, Vulkan veya Direct3D gibi diğer grafik API'leri hakkında bilgi sahibi olmanız gerekmez. Ancak bu API'lerle ilgili deneyiminiz varsa WebGPU ile birçok benzerlik olduğunu fark edebilirsiniz. Bu benzerlikler, öğrenme sürecinize hızlı bir başlangıç yapmanıza yardımcı olabilir.
2. Hazırlanın
Kodu alma
Bu codelab'in herhangi bir bağımlılığı yoktur ve WebGPU uygulamasını oluşturmak için gereken her adımda size yol gösterir. Bu nedenle, başlamak için herhangi bir koda ihtiyacınız yoktur. Ancak, kontrol noktası olarak kullanılabilecek bazı çalışan örnekleri https://github.com/GoogleChromeLabs/your-first-webgpu-app-codelab adresinde bulabilirsiniz. Bu kaynaklara göz atabilir ve takıldığınız noktalarda bunlardan yararlanabilirsiniz.
Geliştirici Konsolu'nu kullanın.
WebGPU, uygun kullanımı zorunlu kılan birçok kurala sahip oldukça karmaşık bir API'dir. Daha da kötüsü, API'nin çalışma şekli nedeniyle birçok hata için normal JavaScript istisnaları oluşturamaz. Bu da sorunun tam olarak nereden kaynaklandığını belirlemeyi zorlaştırır.
WebGPU ile geliştirme yaparken, özellikle de yeni başlıyorsanız sorunlarla karşılaşmanız kaçınılmazdır. API'nin geliştiricileri, GPU geliştirme ile çalışmanın zorluklarının farkındadır ve WebGPU kodunuzun herhangi bir hataya neden olması durumunda, geliştirici konsolunda sorunu belirlemenize ve düzeltmenize yardımcı olacak çok ayrıntılı ve faydalı mesajlar almanızı sağlamak için çok çalışmıştır.
Herhangi bir web uygulamasında çalışırken konsolu açık tutmak her zaman faydalıdır ancak bu durum özellikle burada geçerlidir.
3. WebGPU'yu başlatma
<canvas>
ile başlayın
WebGPU, yalnızca hesaplama yapmak için kullanılmak isteniyorsa ekranda herhangi bir şey göstermeden kullanılabilir. Ancak codelab'de yapacağımız gibi bir şey oluşturmak istiyorsanız tuval kullanmanız gerekir. Bu nedenle, başlamak için iyi bir nokta.
İçinde tek bir <canvas>
öğesi bulunan yeni bir HTML dokümanı oluşturun. Ayrıca, tuval öğesine sorgu gönderdiğimiz bir <script>
etiketi de oluşturun. (Veya 00-starter-page.html dosyasını kullanın.)
- Aşağıdaki kodu kullanarak bir
index.html
dosyası oluşturun:
index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>WebGPU Life</title>
</head>
<body>
<canvas width="512" height="512"></canvas>
<script type="module">
const canvas = document.querySelector("canvas");
// Your WebGPU code will begin here!
</script>
</body>
</html>
Adaptör ve cihaz isteğinde bulunma
Artık WebGPU'nun ayrıntılarına girebilirsiniz. Öncelikle WebGPU gibi API'lerin tüm web ekosistemine yayılmasının biraz zaman alabileceğini göz önünde bulundurmanız gerekir. Bu nedenle, ilk önleyici adım olarak kullanıcının tarayıcısının WebGPU'yu kullanıp kullanamadığını kontrol etmek iyi bir fikirdir.
- WebGPU'nun giriş noktası olarak işlev gören
navigator.gpu
nesnesinin mevcut olup olmadığını kontrol etmek için aşağıdaki kodu ekleyin:
index.html
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
}
İdeal olarak, sayfanın WebGPU kullanmayan bir moda geri dönmesini sağlayarak WebGPU kullanılamıyorsa kullanıcıyı bilgilendirmeniz gerekir. (Belki bunun yerine WebGL kullanılabilir?) Ancak bu codelab'in amacı doğrultusunda, kodun daha fazla yürütülmesini durdurmak için yalnızca bir hata atarsınız.
WebGPU'nun tarayıcı tarafından desteklendiğini öğrendikten sonra, uygulamanız için WebGPU'yu başlatmanın ilk adımı GPUAdapter
istemektir. Adaptörü, cihazınızdaki belirli bir GPU donanımının WebGPU'daki temsili olarak düşünebilirsiniz.
- Bağdaştırıcı almak için
navigator.gpu.requestAdapter()
yöntemini kullanın. Bir söz döndürdüğünden,await
ile çağrılması en uygun yöntemdir.
index.html
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("No appropriate GPUAdapter found.");
}
Uygun bağdaştırıcı bulunamazsa döndürülen adapter
değeri null
olabilir. Bu nedenle, bu olasılığı ele almanız gerekir. Bu durum, kullanıcının tarayıcısı WebGPU'yu destekliyorsa ancak GPU donanımında WebGPU'yu kullanmak için gereken tüm özellikler yoksa meydana gelebilir.
Çoğu zaman, tarayıcının varsayılan bir bağdaştırıcı seçmesine izin vermek (burada yaptığınız gibi) yeterlidir. Ancak daha gelişmiş ihtiyaçlar için requestAdapter()
öğesine iletebileceğiniz bağımsız değişkenler vardır. Bu bağımsız değişkenler, birden fazla GPU'ya sahip cihazlarda (ör. bazı dizüstü bilgisayarlar) düşük güç veya yüksek performanslı donanım kullanmak isteyip istemediğinizi belirtir.
Bir bağdaştırıcınız olduğunda GPU ile çalışmaya başlamadan önceki son adım GPUDevice istemektir. Cihaz, GPU ile etkileşimin en çok gerçekleştiği ana arayüzdür.
adapter.requestDevice()
numaralı telefonu arayarak cihazı alın. Bu işlem, bir söz de döndürür.
index.html
const device = await adapter.requestDevice();
requestAdapter()
'da olduğu gibi, belirli donanım özelliklerini etkinleştirme veya daha yüksek sınırlar isteme gibi daha gelişmiş kullanımlar için geçirilebilecek seçenekler vardır ancak varsayılanlar sizin amaçlarınız için yeterlidir.
Canvas'ı yapılandırma
Artık bir cihazınız olduğuna göre, sayfada bir şeyler göstermek için kullanmak istiyorsanız yapmanız gereken bir şey daha var: Tuvali, yeni oluşturduğunuz cihazla kullanılacak şekilde yapılandırmak.
- Bunu yapmak için önce
canvas.getContext("webgpu")
işlevini çağırarak tuvaldenGPUCanvasContext
isteyin. (Bu, sırasıyla2d
vewebgl
bağlam türlerini kullanarak Canvas 2D veya WebGL bağlamlarını başlatmak için kullandığınız aynı çağrıdır.) Döndürdüğücontext
,configure()
yöntemi kullanılarak cihazla ilişkilendirilmelidir. Örneğin:
index.html
const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device: device,
format: canvasFormat,
});
Burada iletilebilecek birkaç seçenek vardır ancak en önemlileri, bağlamı kullanacağınız device
ve bağlamın kullanması gereken doku biçimi olan format
'dir.
Dokular, WebGPU'nun resim verilerini depolamak için kullandığı nesnelerdir ve her dokunun, GPU'nun bu verilerin bellekte nasıl düzenlendiğini bilmesini sağlayan bir biçimi vardır. Doku belleğinin işleyiş şekliyle ilgili ayrıntılar bu codelab'in kapsamı dışındadır. Bilmeniz gereken önemli nokta, tuval bağlamının kodunuzun içine çizmesi için dokular sağladığı ve kullandığınız biçimin, tuvalin bu resimleri ne kadar verimli bir şekilde gösterebileceği üzerinde etkili olabileceğidir. Farklı cihaz türleri, farklı doku biçimleri kullanıldığında en iyi performansı gösterir. Cihazın tercih ettiği biçimi kullanmazsanız resim sayfanın bir parçası olarak gösterilmeden önce arka planda fazladan bellek kopyaları oluşturulabilir.
Neyse ki WebGPU, tuvaliniz için hangi biçimi kullanmanız gerektiğini size bildirdiği için bunların hiçbiri hakkında endişelenmenize gerek yok. Neredeyse tüm durumlarda, yukarıda gösterildiği gibi navigator.gpu.getPreferredCanvasFormat()
çağrılarak döndürülen değeri iletmek istersiniz.
Tuvali temizleme
Artık bir cihazınız olduğuna ve tuval bu cihazla yapılandırıldığına göre tuvalin içeriğini değiştirmek için cihazı kullanmaya başlayabilirsiniz. Başlamak için arka planı düz bir renkle temizleyin.
Bunu veya WebGPU'daki diğer işlemleri yapabilmek için GPU'ya ne yapması gerektiğini söyleyen bazı komutlar sağlamanız gerekir.
- Bunu yapmak için cihazın, GPU komutlarını kaydetmek için bir arayüz sağlayan
GPUCommandEncoder
oluşturmasını sağlayın.
index.html
const encoder = device.createCommandEncoder();
GPU'ya göndermek istediğiniz komutlar oluşturmayla (bu durumda tuvali temizleme) ilgili olduğundan bir sonraki adım, oluşturma geçişi başlatmak için encoder
kullanmaktır.
Render geçişleri, WebGPU'daki tüm çizim işlemlerinin gerçekleştiği zamanlardır. Her biri, gerçekleştirilen tüm çizim komutlarının çıkışını alan dokuları tanımlayan bir beginRenderPass()
çağrısıyla başlar. Daha gelişmiş kullanımlar, oluşturulan geometrinin derinliğini depolama veya kenarları yumuşatma gibi çeşitli amaçlara yönelik ekler adı verilen birkaç doku sağlayabilir. Ancak bu uygulama için yalnızca bir tane gerekir.
context.getCurrentTexture()
işlevini çağırarak daha önce oluşturduğunuz tuval bağlamından dokuyu alın. Bu işlev, tuvalinwidth
veheight
özellikleriyle eşleşen piksel genişliği ve yüksekliğine sahip bir doku döndürür. Ayrıca,context.configure()
işlevini çağırdığınızda belirtilenformat
değerini de döndürür.
index.html
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
storeOp: "store",
}]
});
Doku, colorAttachment
öğesinin view
özelliği olarak verilir. Render geçişleri, dokunun hangi kısımlarının oluşturulacağını belirten bir GPUTexture
yerine GPUTextureView
sağlamanızı gerektirir. Bu durum yalnızca daha gelişmiş kullanım alanlarında önemlidir. Bu nedenle, burada doku üzerinde herhangi bir bağımsız değişken olmadan createView()
çağrısı yaparsınız. Bu, oluşturma geçişinin dokunun tamamını kullanmasını istediğinizi gösterir.
Ayrıca, oluşturma geçişinin başladığında ve bittiğinde dokuyla ne yapmasını istediğinizi de belirtmeniz gerekir:
loadOp
değeri"clear"
ise oluşturma geçişi başladığında dokunun temizlenmesini istediğinizi gösterir.storeOp
değeri"store"
olduğunda, oluşturma geçişi tamamlandıktan sonra oluşturma geçişi sırasında yapılan tüm çizimlerin sonuçlarının dokuya kaydedilmesini istediğiniz belirtilir.
Render geçişi başladıktan sonra hiçbir şey yapmayın. En azından şimdilik. Oluşturma geçişini loadOp: "clear"
ile başlatmak, doku görünümünü ve tuvali temizlemek için yeterlidir.
- Aşağıdaki çağrıyı
beginRenderPass()
'dan hemen sonra ekleyerek oluşturma geçişini sonlandırın:
index.html
pass.end();
Bu çağrıları yapmanın GPU'nun herhangi bir işlem yapmasına neden olmadığını bilmek önemlidir. Bunlar, GPU'nun daha sonra yapması için kaydedilen komutlardır.
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();
queue
GPUDevice
kullanarak komut arabelleğini GPU'ya gönderin. Kuyruk, tüm GPU komutlarını gerçekleştirerek yürütülmelerinin iyi bir şekilde sıralanmasını ve düzgün şekilde senkronize edilmesini sağlar. Kuyruğunsubmit()
yöntemi, bu durumda yalnızca bir tane olsa da bir komut arabellekleri dizisi alır.
index.html
device.queue.submit([commandBuffer]);
Gönderdiğiniz komut arabelleği tekrar kullanılamaz. Bu nedenle, arabelleği saklamanıza gerek yoktur. Daha fazla komut göndermek istiyorsanız başka bir komut arabelleği oluşturmanız gerekir. Bu nedenle, bu iki adımın tek bir adımda birleştirilmesi oldukça yaygındır. Bu codelab'in örnek sayfalarında da bu şekilde yapılmıştır:
index.html
// Finish the command buffer and immediately submit it.
device.queue.submit([encoder.finish()]);
Komutları GPU'ya gönderdikten sonra JavaScript'in kontrolü tarayıcıya geri vermesine izin verin. Bu noktada tarayıcı, bağlamın mevcut dokusunu değiştirdiğinizi görür ve tuvali, bu dokuyu resim olarak gösterecek şekilde günceller. Bundan sonra tuval içeriklerini tekrar güncellemek isterseniz context.getCurrentTexture()
işlevini tekrar çağırarak yeni bir doku almak için yeni bir komut arabelleği kaydedip göndermeniz gerekir.
- Sayfayı tekrar yükleyin. Tuvalin siyah renkle doldurulduğunu fark edin. Tebrikler! Bu, ilk WebGPU uygulamanızı başarıyla oluşturduğunuz anlamına gelir.
Bir renk seçin.
Ancak dürüst olmak gerekirse siyah kareler oldukça sıkıcı. Bu nedenle, bir sonraki bölüme geçmeden önce biraz zaman ayırarak bu bölümü kişiselleştirin.
encoder.beginRenderPass()
çağrısında,colorAttachment
öğesineclearValue
ile yeni bir satır ekleyin. Örneğin:
index.html
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1 }, // New line
storeOp: "store",
}],
});
clearValue
, geçişin başında clear
işlemi gerçekleştirilirken hangi rengin kullanılacağını oluşturma geçişine bildirir. İçine iletilen sözlük dört değer içerir: kırmızı için r
, yeşil için g
, mavi için b
ve alfa (şeffaflık) için a
. Her değer 0
ile 1
arasında olabilir ve birlikte bu renk kanalının değerini tanımlar. Örneğin:
{ r: 1, g: 0, b: 0, a: 1 }
parlak kırmızıdır.{ r: 1, g: 0, b: 1, a: 1 }
parlak mordur.{ r: 0, g: 0.3, b: 0, a: 1 }
koyu yeşildir.{ r: 0.5, g: 0.5, b: 0.5, a: 1 }
orta gri renktedir.{ r: 0, g: 0, b: 0, a: 0 }
, varsayılan saydam siyahtır.
Bu codelab'deki örnek kod ve ekran görüntüleri koyu mavi renkte olsa da istediğiniz rengi seçebilirsiniz.
- 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 tuval üzerine basit bir geometri (renkli bir kare) çizecek. Bu kadar basit bir çıktı için çok fazla iş yapıyormuş gibi görüneceğini şimdiden belirtelim. Bunun nedeni, WebGPU'nun çok sayıda geometriyi çok verimli bir şekilde oluşturmak üzere tasarlanmış olmasıdır. Bu verimliliğin bir yan etkisi olarak, nispeten basit şeyleri yapmak alışılmadık derecede zor gelebilir. Ancak WebGPU gibi bir API'ye yöneliyorsanız biraz daha karmaşık bir şey yapmak istediğiniz için bu beklentiye girersiniz.
GPU'ların nasıl çizim yaptığını anlama
Daha fazla kod değişikliği yapmadan önce, GPU'ların ekranda gördüğünüz şekilleri nasıl oluşturduğuyla ilgili çok hızlı, basitleştirilmiş ve üst düzey bir genel bakış yapmanız faydalı olacaktır. (GPU oluşturmanın temel işleyiş şeklini biliyorsanız Köşeleri Tanımlama bölümüne geçebilirsiniz.)
Canvas 2D gibi, kullanıma hazır birçok şekil ve seçenek sunan bir API'nin aksine GPU'nuz yalnızca birkaç farklı şekil türüyle (veya WebGPU'nun kullandığı terimle ilkel) ilgilenir: noktalar, çizgiler ve üçgenler. Bu codelab'de yalnızca üçgenler kullanacaksınız.
Üçgenler, tahmin edilebilir ve verimli bir şekilde işlenmelerini sağlayan birçok güzel matematiksel özelliğe sahip oldukları için GPU'lar neredeyse yalnızca üçgenlerle çalışır. GPU ile çizdiğiniz hemen her şeyin, GPU tarafından çizilebilmesi için üçgenlere bölünmesi gerekir. Bu üçgenler de köşe noktalarıyla tanımlanmalıdır.
Bu noktalar veya köşeler, WebGPU ya da benzer API'ler tarafından tanımlanan bir Kartezyen koordinat sistemindeki bir noktayı tanımlayan X, Y ve (3D içerik için) Z değerleri açısından verilir. Koordinat sisteminin yapısını en kolay şekilde sayfanızdaki tuvalle ilişkisi açısından düşünebilirsiniz. Tuvaliniz ne kadar geniş veya uzun olursa olsun, sol kenar her zaman X ekseninde -1, sağ kenar ise her zaman X ekseninde +1 olur. Benzer şekilde, alt kenar Y ekseninde her zaman -1, üst kenar ise Y ekseninde her zaman +1 olur. Bu durumda (0, 0) her zaman tuvalin merkezi, (-1, -1) her zaman sol alt köşe ve (1, 1) her zaman sağ üst köşe olur. Bu, Kırpma Alanı olarak bilinir.
Köşeler başlangıçta bu koordinat sisteminde nadiren tanımlanır. Bu nedenle GPU'lar, köşeleri kırpma alanına dönüştürmek için gereken matematik işlemlerini ve köşeleri çizmek için gereken diğer hesaplamaları yapmak üzere köşe gölgelendiriciler adı verilen küçük programları kullanır. Örneğin, gölgelendirici bazı animasyonlar uygulayabilir veya köşeden ışık kaynağına olan yönü hesaplayabilir. Bu gölgelendiriciler, WebGPU geliştiricisi olarak sizin tarafınızdan yazılır ve GPU'nun çalışma şekli üzerinde inanılmaz bir kontrol sağlar.
GPU, bu dönüştürülmüş köşelerden oluşan tüm üçgenleri alır ve ekranda bunları çizmek için hangi piksellerin gerektiğini belirler. Ardından, her pikselin hangi renkte olması gerektiğini hesaplayan parça gölgelendirici adlı küçük bir program daha çalıştırır. Bu hesaplama, yeşil döndür kadar basit veya yüzeyin, yakındaki diğer yüzeylerden yansıyan, sisle filtrelenen ve yüzeyin ne kadar metalik olduğuna göre değiştirilen güneş ışığına göre açısını hesaplamak kadar karmaşık olabilir. Tamamen sizin kontrolünüzdedir. Bu durum hem güçlendirici hem de bunaltıcı olabilir.
Bu piksel renklerinin sonuçları daha sonra bir dokuda toplanır ve ekranda gösterilebilir.
Köşeleri tanımlama
Daha önce de belirtildiği gibi, Yaşam Oyunu simülasyonu hücrelerden oluşan bir ızgara olarak gösterilir. Uygulamanızda, etkin hücreleri etkin olmayan hücrelerden ayıran bir şekilde ızgarayı görselleştirme yöntemi olmalıdır. Bu codelab'de kullanılan yaklaşım, etkin hücrelere renkli kareler çizmek ve etkin olmayan hücreleri boş bırakmaktır.
Bu durumda, GPU'yu dört farklı noktayla (karenin dört köşesi için birer nokta) sağlamanız gerekir. Örneğin, tuvalin ortasına çizilen ve kenarlardan biraz çekilen bir karenin köşe koordinatları şu şekildedir:
Bu koordinatları GPU'ya aktarmak için değerleri bir TypedArray'e yerleştirmeniz gerekir. TypedArray'ler, bitişik bellek blokları ayırmanıza ve serideki her öğeyi belirli bir veri türü olarak yorumlamanıza olanak tanıyan bir JavaScript nesneleri grubudur. Örneğin, Uint8Array
içinde dizideki her öğe tek bir işaretsiz bayttır. TypedArray'ler, WebAssembly, WebAudio ve (elbette) WebGPU gibi bellek düzenine duyarlı API'lerle veri göndermek ve almak için idealdir.
Kare örneğinde, değerler kesirli olduğundan Float32Array
uygundur.
- Aşağıdaki dizi bildirimini kodunuza yerleştirerek diyagramdaki tüm köşe konumlarını tutan bir dizi oluşturun. Bu kodu, üst kısma yakın bir yere,
context.configure()
çağrısının hemen altına yerleştirebilirsiniz.
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8,
0.8, -0.8,
0.8, 0.8,
-0.8, 0.8,
]);
Boşluk ve yorumun değerler üzerinde etkisi olmadığını, yalnızca kolaylık sağlamak ve daha okunabilir hale getirmek için kullanıldığını unutmayın. Her değer çiftinin bir köşe için X ve Y koordinatlarını oluşturduğunu görmenize yardımcı olur.
Ancak bir sorun var. GPU'lar üçgenler halinde çalışır, değil mi? Bu nedenle, köşeleri üçlü gruplar halinde sağlamanız gerekir. Dört kişilik bir grubunuz var. Çözüm, karenin ortasından geçen bir kenarı paylaşan iki üçgen oluşturmak için köşelerden ikisini tekrarlamaktır.
Şekildeki kareyi oluşturmak için (-0.8, -0.8) ve (0.8, 0.8) köşe noktalarını iki kez listelemeniz gerekir. Bir kez mavi üçgen, bir kez de kırmızı üçgen için. (Kareyi diğer iki köşeyle de bölebilirsiniz. Bu, sonucu değiştirmez.)
- Önceki
vertices
dizinizi aşağıdaki gibi görünecek şekilde güncelleyin:
index.html
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red)
0.8, 0.8,
-0.8, 0.8,
]);
Şemada netlik için iki üçgen arasında ayrım gösterilse de köşe konumları tam olarak aynıdır ve GPU bunları boşluksuz olarak oluşturur. Tek bir dolu kare olarak oluşturulur.
Köşe arabelleği oluşturma
GPU, JavaScript dizisindeki verilerle köşe çizemiyor. GPU'lar genellikle oluşturma için son derece optimize edilmiş kendi belleklerine sahiptir. Bu nedenle, GPU'nun çizim yaparken kullanmasını istediğiniz tüm verilerin bu belleğe yerleştirilmesi gerekir.
Köşe verileri de dahil olmak üzere birçok değer için GPU tarafındaki bellek GPUBuffer
nesneleri aracılığıyla yönetilir. Arabellek, GPU'nun kolayca erişebildiği ve belirli amaçlar için işaretlenmiş bir bellek bloğudur. Bunu biraz GPU'nun görebildiği bir TypedArray olarak düşünebilirsiniz.
- Köşelerinizi tutacak bir arabellek oluşturmak için
vertices
dizinizin tanımından sonradevice.createBuffer()
öğesine aşağıdaki çağrıyı ekleyin.
index.html
const vertexBuffer = device.createBuffer({
label: "Cell vertices",
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
İlk olarak, arabelleğe bir etiket verdiğinizi fark edeceksiniz. Oluşturduğunuz her WebGPU nesnesine isteğe bağlı bir etiket verebilirsiniz. Bunu yapmanız kesinlikle önerilir. Etiket, nesnenin ne olduğunu belirlemenize yardımcı olduğu sürece istediğiniz dize olabilir. Herhangi bir sorunla karşılaşırsanız WebGPU'nun ürettiği hata mesajlarında bu etiketler kullanılır. Böylece, neyin yanlış gittiğini anlayabilirsiniz.
Ardından, arabelleğin boyutunu bayt cinsinden girin. 32 bitlik bir kayan sayının boyutunu ( 4 bayt) vertices
dizinizdeki kayan sayıların sayısıyla (12) çarparak belirlediğiniz 48 baytlık bir arabellek gerekir. Neyse ki TypedArray'ler byteLength değerini sizin için hesaplar. Bu nedenle, arabellek oluştururken bu değeri kullanabilirsiniz.
Son olarak, arabelleğin kullanımını belirtmeniz gerekir. Bu, |
( bit düzeyinde VEYA) operatörüyle birleştirilmiş birden fazla işaretle birlikte bir veya daha fazla GPUBufferUsage
işaretidir. Bu durumda, arabelleğin köşe verileri için kullanılmasını (GPUBufferUsage.VERTEX
) ve verileri arabelleğe kopyalayabilmeyi (GPUBufferUsage.COPY_DST
) istediğinizi belirtirsiniz.
Size döndürülen arabellek nesnesi opak olduğundan içerdiği verileri (kolayca) inceleyemezsiniz. Ayrıca, çoğu özelliği sabittir. Bir GPUBuffer
oluşturulduktan sonra yeniden boyutlandıramaz veya kullanım işaretlerini değiştiremezsiniz. Değiştirebileceğiniz şey, belleğindeki içeriklerdir.
Arabellek ilk oluşturulduğunda, içerdiği bellek sıfır olarak başlatılır. İçeriğini değiştirmenin çeşitli yolları vardır ancak en kolayı, kopyalamak istediğiniz bir TypedArray ile device.queue.writeBuffer()
işlevini çağırmaktır.
- Köşe verilerini arabelleğin belleğine kopyalamak için aşağıdaki kodu ekleyin:
index.html
device.queue.writeBuffer(vertexBuffer, /*bufferOffset=*/0, vertices);
Köşe düzenini tanımlama
Artık içinde köşe verileri olan bir arabelleğiniz var ancak GPU açısından bu yalnızca bir bayt blobu. Bu araçla bir şeyler çizecekseniz biraz daha bilgi vermeniz gerekir. WebGPU'ya köşe verilerinin yapısı hakkında daha fazla bilgi vermeniz gerekir.
- Köşe verisi yapısını
GPUVertexBufferLayout
sözlüğüyle tanımlayın:
index.html
const vertexBufferLayout = {
arrayStride: 8,
attributes: [{
format: "float32x2",
offset: 0,
shaderLocation: 0, // Position, see vertex shader
}],
};
Bu, ilk bakışta biraz kafa karıştırıcı olabilir ancak nispeten kolayca parçalara ayrılabilir.
İlk verdiğiniz şey arrayStride
olur. Bu, GPU'nun bir sonraki tepe noktasını ararken arabellekte ileriye doğru atlaması gereken bayt sayısıdır. Karenizin her köşesi, 32 bitlik iki kayan nokta sayısından oluşur. Daha önce de belirtildiği gibi, 32 bitlik bir kayan nokta sayısı 4 bayt olduğundan iki kayan nokta sayısı 8 bayttır.
Sırada dizi olan attributes
özelliği var. Özellikler, her bir köşeye kodlanmış ayrı bilgi parçalarıdır. Köşeleriniz yalnızca bir özellik (köşe konumu) içerir ancak daha gelişmiş kullanım alanlarında genellikle köşelerin rengi veya geometrik yüzeyin yönü gibi birden fazla özellik içeren köşeler bulunur. Ancak bu, bu codelab'in kapsamı dışındadır.
Tek özelliğinizde önce verilerin format
tanımlanır. Bu, GPU'nun anlayabileceği her bir köşe verisi türünü açıklayan GPUVertexFormat
türlerinin listesinden gelir. Köşelerinizin her biri 32 bitlik iki kayan nokta içerdiğinden float32x2
biçimini kullanırsınız. Köşe verileriniz bunun yerine her biri dört 16 bitlik işaretsiz tam sayıdan oluşuyorsa örneğin uint16x4
kullanırsınız. Deseni görüyor musunuz?
Ardından, offset
, bu özelliğin köşe noktasında kaç bayt sonra başladığını açıklar. Bu konuda endişelenmeniz gereken tek durum, arabelleğinizde birden fazla özellik olmasıdır. Bu durum, bu codelab sırasında ortaya çıkmaz.
Son olarak, shaderLocation
'ı kullanabilirsiniz. Bu, 0 ile 15 arasında rastgele bir sayıdır ve tanımladığınız her özellik için benzersiz olmalıdır. Bu özellik, bir sonraki bölümde öğreneceğiniz köşe gölgelendirici içindeki belirli bir girişe bağlanır.
Bu değerleri şimdi tanımlamanıza rağmen henüz WebGPU API'ye aktarmadığınızı unutmayın. Bu değerler daha sonra kullanılacak olsa da bunları en kolay şekilde köşelerinizi tanımladığınız noktada düşünebilirsiniz.
Gölgelendiricilerle başlama
Artık oluşturmak istediğiniz verilere sahipsiniz ancak GPU'ya bu verileri tam olarak nasıl işleyeceğini söylemeniz gerekiyor. Bunun büyük bir kısmı gölgelendiricilerle gerçekleşir.
Gölgelendiriciler, yazdığınız ve GPU'nuzda yürütülen küçük programlardır. Her gölgelendirici, verilerin farklı bir aşamasında çalışır: Köşe işleme, Parça işleme veya genel Hesaplama. GPU'da bulundukları için ortalama JavaScript'ten daha katı bir şekilde yapılandırılırlar. Ancak bu yapı, çok hızlı ve en önemlisi paralel olarak yürütmelerini sağlar.
WebGPU'daki gölgelendiriciler, WGSL (WebGPU Shading Language) adlı bir gölgelendirme dilinde yazılır. WGSL, söz dizimi açısından Rust'a biraz benzer. Ortak GPU işlerinin (ör. vektör ve matris matematiği) daha kolay ve hızlı yapılmasını amaçlayan özelliklere sahiptir. Gölgelendirme dilinin tamamını öğretmek bu codelab'in kapsamı dışındadır ancak umarız bazı basit örnekleri incelerken temel bilgileri edinebilirsiniz.
Gölgelendiriciler, WebGPU'ya dizeler olarak aktarılır.
- Aşağıdakileri
vertexBufferLayout
işaretinin altındaki kodunuza kopyalayarak gölgelendirici kodunuzu girebileceğiniz bir yer oluşturun:
index.html
const cellShaderModule = device.createShaderModule({
label: "Cell shader",
code: `
// Your shader code will go here
`
});
Gölgelendiricileri oluşturmak için device.createShaderModule()
işlevini çağırırsınız. Bu işlev için isteğe bağlı olarak label
ve WGSL code
dizesini sağlarsınız. (Çok satırlı dizelere izin vermek için burada ters tırnak kullandığınızı unutmayın.) Geçerli bir WGSL kodu eklediğinizde işlev, derlenmiş sonuçları içeren bir GPUShaderModule
nesnesi döndürür.
Köşe gölgelendiricisini tanımlayın
GPU da aynı noktadan başladığı için köşe gölgelendiriciden başlayın.
Köşe gölgelendirici bir işlev olarak tanımlanır ve GPU, bu işlevi vertexBuffer
öğenizdeki her köşe için bir kez çağırır. vertexBuffer
şeklinizde altı konum (köşe) olduğundan, tanımladığınız işlev altı kez çağrılır. Her çağrıldığında, vertexBuffer
öğesinden farklı bir konum işleve bağımsız değişken olarak iletilir ve klip alanında karşılık gelen bir konumu döndürmek köşe gölgelendirici işlevinin görevidir.
Bu işlevlerin sırayla çağrılmayabileceğini anlamak önemlidir. Bunun yerine, GPU'lar bu tür gölgelendiricileri paralel olarak çalıştırmada mükemmeldir ve aynı anda yüzlerce (hatta binlerce!) köşe işleyebilir. Bu, GPU'ların inanılmaz hızının büyük bir bölümünü oluşturur ancak sınırlamaları vardır. Aşırı paralelleştirme sağlamak için köşe gölgelendiriciler birbirleriyle iletişim kuramaz. Her gölgelendirici çağrısı, tek seferde yalnızca tek bir köşe için verileri görebilir ve yalnızca tek bir köşe için değerler çıkışı yapabilir.
WGSL'de, köşe gölgelendirici işlevine istediğiniz adı verebilirsiniz ancak hangi gölgelendirici aşamasını temsil ettiğini belirtmek için önüne @vertex
özelliği eklenmelidir. WGSL, işlevleri fn
anahtar kelimesiyle belirtir, bağımsız değişkenleri bildirmek için parantezleri ve kapsamı tanımlamak için küme parantezlerini kullanır.
- Aşağıdaki gibi boş bir
@vertex
işlevi oluşturun:
index.html (createShaderModule kodu)
@vertex
fn vertexMain() {
}
Ancak bir köşe gölgelendiricisi, klip alanında işlenen köşenin en azından son konumunu döndürmesi gerektiğinden bu geçerli değildir. Bu değer her zaman 4 boyutlu bir vektör olarak verilir. Vektörler, gölgelendiricilerde çok yaygın olarak kullanılan bir şeydir. Bu nedenle, dilde birinci sınıf temel öğeler olarak kabul edilir ve 4 boyutlu bir vektör için vec4f
gibi kendi türleri vardır. 2D vektörler (vec2f
) ve 3D vektörler (vec3f
) için de benzer türler vardır.
- Döndürülen değerin gerekli konum olduğunu belirtmek için
@builtin(position)
özelliğiyle işaretleyin. İşlevin döndürdüğü değeri belirtmek için->
simgesi kullanılır.
index.html (createShaderModule kodu)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
}
Elbette, işlevin bir dönüş türü varsa işlev gövdesinde gerçekten bir değer döndürmeniz gerekir. vec4f(x, y, z, w)
söz dizimini kullanarak döndürülecek yeni bir vec4f
oluşturabilirsiniz. x
, y
ve z
değerleri, döndürülen değerde köşe noktasının kırpma alanında nerede bulunduğunu gösteren kayan nokta sayılardır.
(0, 0, 0, 1)
statik değerini döndürdüğünüzde, GPU ürettiği üçgenlerin tek bir noktadan ibaret olduğunu fark edip bunları sildiğinden hiçbir zaman bir şey görüntülemeyen geçerli bir köşe gölgelendiriciniz olur.
index.html (createShaderModule kodu)
@vertex
fn vertexMain() -> @builtin(position) vec4f {
return vec4f(0, 0, 0, 1); // (X, Y, Z, W)
}
Bunun yerine, oluşturduğunuz arabellekteki verileri kullanmak istersiniz. Bunu da @location()
özelliği ve vertexBufferLayout
içinde açıkladığınızla eşleşen bir türle işleviniz için bir bağımsız değişken tanımlayarak yaparsınız. shaderLocation
değerini 0
olarak belirttiniz. Bu nedenle, WGSL kodunuzda bağımsız değişkeni @location(0)
ile işaretleyin. Biçimi float32x2
olarak da tanımladınız. Bu, 2 boyutlu bir vektördür. Dolayısıyla WGSL'de bağımsız değişkeniniz vec2f
olur. İstediğiniz adı verebilirsiniz ancak bunlar köşe konumlarınızı temsil ettiğinden pos gibi bir ad kullanmak mantıklı olacaktır.
- Gölgeleyici işlevinizi aşağıdaki kodla değiştirin:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(0, 0, 0, 1);
}
Şimdi de bu konuma geri dönmeniz gerekiyor. Konum 2 boyutlu bir vektör, dönüş türü ise 4 boyutlu bir vektör olduğundan bunu biraz değiştirmeniz gerekir. Yapmak istediğiniz şey, konum bağımsız değişkenindeki iki bileşeni alıp döndürülen vektörün ilk iki bileşenine yerleştirmek, son iki bileşeni ise sırasıyla 0
ve 1
olarak bırakmaktır.
- Hangi konum bileşenlerinin kullanılacağını açıkça belirterek doğru konumu döndürün:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos.x, pos.y, 0, 1);
}
Ancak bu tür eşlemeler gölgelendiricilerde çok yaygın olduğundan konum vektörünü uygun bir kısaltma ile ilk bağımsız değişken olarak da iletebilirsiniz. Bu, aynı anlama gelir.
return
ifadesini aşağıdaki kodla yeniden yazın:
index.html (createShaderModule kodu)
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
İlk köşe gölgelendiriciniz hazır! Bu yöntem çok basittir. Konum, neredeyse hiç değiştirilmeden iletilir ancak başlangıç için yeterlidir.
Parça gölgelendiriciyi tanımlama
Sırada parça gölgelendirici var. Parça gölgelendiriciler, köşe gölgelendiricilere çok benzer şekilde çalışır ancak her köşe için çağrılmak yerine çizilen her piksel için çağrılırlar.
Parça gölgelendiriciler her zaman köşe gölgelendiricilerden sonra çağrılır. GPU, köşe gölgelendiricilerin çıkışını alır ve üçgenleştirir. Böylece üç noktalı kümelerden üçgenler oluşturur. Ardından, çıkış rengi eklerinin hangi piksellerinin bu üçgene dahil edildiğini belirleyerek bu üçgenlerin her birini rasterleştirir ve bu piksellerin her biri için parça gölgelendiriciyi bir kez çağırır. Parça gölgelendirici, genellikle köşe gölgelendiriciden ve dokular gibi öğelerden kendisine gönderilen değerlerden hesaplanan bir renk döndürür. Bu renk, GPU tarafından renk ekine yazılır.
Parça gölgelendiriciler de tıpkı köşe gölgelendiriciler gibi büyük ölçüde paralel bir şekilde yürütülür. Giriş ve çıkışları açısından köşe gölgelendiricilerden biraz daha esnektirler ancak her üçgenin her pikseli için tek bir renk döndürdüklerini düşünebilirsiniz.
WGSL parça gölgelendirici işlevi, @fragment
özelliğiyle gösterilir ve vec4f
döndürür. Ancak bu durumda vektör, konumu değil rengi temsil eder. Dönüş değerine, beginRenderPass
çağrısından hangi colorAttachment
'ye döndürülen rengin yazıldığını belirtmek için @location
özelliği verilmesi gerekir. Yalnızca bir ek olduğu için konum 0'dır.
- Aşağıdaki gibi boş bir
@fragment
işlevi oluşturun:
index.html (createShaderModule kodu)
@fragment
fn fragmentMain() -> @location(0) vec4f {
}
Döndürülen vektörün dört bileşeni, kırmızı, yeşil, mavi ve alfa renk değerleridir. Bu değerler, daha önce beginRenderPass
içinde ayarladığınız clearValue
ile tam olarak aynı şekilde yorumlanır. Bu nedenle vec4f(1, 0, 0, 1)
parlak kırmızıdır ve bu, kareniz için uygun bir renk gibi görünmektedir. Ancak istediğiniz renge ayarlayabilirsiniz.
- Döndürülen renk vektörünü şu şekilde ayarlayın:
index.html (createShaderModule kodu)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1); // (Red, Green, Blue, Alpha)
}
Bu da eksiksiz bir parça gölgelendiricidir. Bu, çok ilginç bir örnek değildir. Yalnızca her üçgenin her pikselini kırmızıya ayarlar ancak bu, şimdilik yeterlidir.
Özetlemek gerekirse, yukarıda ayrıntılı olarak açıklanan gölgelendirici kodunu ekledikten sonra createShaderModule
çağrınız artık şu şekilde görünür:
index.html
const cellShaderModule = device.createShaderModule({
label: 'Cell shader',
code: `
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos, 0, 1);
}
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
`
});
Render ardışık düzeni oluşturma
Bir gölgelendirici modülü, tek başına oluşturma için kullanılamaz. Bunun yerine, device.createRenderPipeline() çağrılarak oluşturulan bir GPURenderPipeline
parçası olarak kullanmanız gerekir. Oluşturma işlem hattı, hangi gölgelendiricilerin kullanılacağı, köşe arabelleklerindeki verilerin nasıl yorumlanacağı, hangi tür geometrinin oluşturulması gerektiği (çizgiler, noktalar, üçgenler vb.) gibi öğeler de dahil olmak üzere geometrinin nasıl çizileceğini kontrol eder.
Render işlem hattı, API'nin tamamındaki en karmaşık nesnedir ancak endişelenmeyin. Bu işleme iletebileceğiniz değerlerin çoğu isteğe bağlıdır ve başlamak için yalnızca birkaç değer sağlamanız gerekir.
- Şunun gibi bir oluşturma ardışık düzeni oluşturun:
index.html
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: "auto",
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
Her işlem hattının, işlem hattının ihtiyaç duyduğu giriş türlerini (köşe arabellekleri hariç) açıklayan bir layout
'ye ihtiyacı vardır ancak sizde bu tür bir giriş yoktur. Neyse ki şimdilik "auto"
değerini iletebilirsiniz. Bu durumda, işlem hattı kendi düzenini gölgelendiricilerden oluşturur.
Ardından, vertex
aşamasıyla ilgili ayrıntıları sağlamanız gerekir. module
, köşe gölgelendiricinizi içeren GPUShaderModule'dür ve entryPoint
, her köşe çağrısı için çağrılan gölgelendirici kodundaki işlevin adını verir. (Tek bir gölgelendirici modülünde birden fazla @vertex
ve @fragment
işlevi olabilir.) Buffers, bu ardışık düzenle kullandığınız tepe noktası arabelleklerinde verilerinizin nasıl paketlendiğini açıklayan GPUVertexBufferLayout
nesnelerinden oluşan bir dizidir. Neyse ki bunu vertexBufferLayout
içinde daha önce tanımlamıştınız. Buradan geçebilirsiniz.
Son olarak, fragment
aşamasıyla ilgili ayrıntılar yer alır. Bu, köşe aşaması gibi bir gölgelendirici modülü ve entryPoint'i de içerir. Son olarak, bu işlem hattının kullanıldığı targets
tanımlanır. Bu, işlem hattının çıkışını yaptığı renk eklerinin doku gibi ayrıntılarını veren bir sözlük dizisidir. format
Bu ayrıntıların, bu işlem hattının kullanıldığı tüm oluşturma geçişlerinin colorAttachments
bölümünde verilen dokularla eşleşmesi gerekir. Render geçişiniz, tuval bağlamındaki dokuları kullanır ve biçimi için canvasFormat
içinde kaydettiğiniz değeri kullandığından burada aynı biçimi iletirsiniz.
Bu, oluşturma işlem hattı oluştururken belirtebileceğiniz tüm seçeneklere yakın bile değildir ancak bu codelab'in ihtiyaçları için yeterlidir.
Kareyi çizin
Artık karenizi çizmek için ihtiyacınız olan her şeye sahipsiniz.
- Kareyi çizmek için
encoder.beginRenderPass()
vepass.end()
çağrı çiftine geri dönün, ardından aralarına şu yeni komutları ekleyin:
index.html
// After encoder.beginRenderPass()
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2); // 6 vertices
// before pass.end()
Bu, WebGPU'ya karenizi çizmek için gereken tüm bilgileri sağlar. Öncelikle, hangi işlem hattının çizim için kullanılacağını belirtmek üzere setPipeline()
simgesini kullanırsınız. Kullanılan gölgelendiriciler, köşe verilerinin düzeni ve diğer ilgili durum verileri buna dahildir.
Ardından, karenizin köşelerini içeren arabellek ile setVertexBuffer()
işlevini çağırırsınız. Bu arabellek, mevcut işlem hattının vertex.buffers
tanımındaki 0. öğeye karşılık geldiği için 0
ile çağırırsınız.
Son olarak, tüm kurulum işlemlerinden sonra tuhaf bir şekilde basit görünen draw()
araması yaparsınız. Tek yapmanız gereken, oluşturması gereken köşe sayısını iletmektir. Bu sayı, şu anda ayarlanmış köşe arabelleklerinden alınır ve şu anda ayarlanmış işlem hattıyla yorumlanır. Bunu 6
olarak sabit kodlayabilirsiniz ancak köşe dizisinden (12 kayan nokta / köşe başına 2 koordinat == 6 köşe) hesaplamak, kareyi örneğin bir daireyle değiştirmeye karar verirseniz elle güncellemeniz gereken daha az şey olacağı anlamına gelir.
- Ekranınızı yenileyin ve sıkı çalışmanızın sonucunu (nihayet) görün: büyük bir renkli kare.
5. Izgara çizme
Öncelikle kendinizi tebrik edin. Geometri verilerinin ilk kısımlarını ekrana getirmek, çoğu GPU API'sinde genellikle en zor adımlardan biridir. Buradan itibaren yapacağınız her şeyi daha küçük adımlara bölebilirsiniz. Böylece ilerlemenizi daha kolay doğrulayabilirsiniz.
Bu bölümde şunları öğreneceksiniz:
- Değişkenleri (üniforma olarak adlandırılır) JavaScript'ten gölgelendiriciye aktarma
- Render davranışını değiştirmek için tek tip kullanma.
- Aynı geometrinin birçok farklı varyantını çizmek için örnekleme nasıl kullanılır?
Izgarayı tanımlama
Bir ızgarayı oluşturmak için ızgarayla ilgili çok temel bir bilgiyi bilmeniz gerekir. Genişlik ve yükseklik olarak kaç hücre içeriyor? Bu, geliştirici olarak size bağlıdır ancak işleri biraz daha kolaylaştırmak için ızgarayı kare (aynı genişlik ve yükseklik) olarak değerlendirin ve iki katı olan bir boyut kullanın. (Bu, daha sonra bazı matematik işlemlerini kolaylaştırır.) Sonunda daha büyük bir ızgara oluşturmak istiyorsunuz ancak bu bölümün geri kalanında ızgara boyutunuzu 4x4 olarak ayarlayın. Bu, bu bölümde kullanılan bazı matematiksel işlemleri göstermeyi kolaylaştırır. Daha sonra ölçeği artırın.
- JavaScript kodunuzun en üstüne bir sabit ekleyerek ızgara boyutunu tanımlayın.
index.html
const GRID_SIZE = 4;
Ardından, tuval üzerine GRID_SIZE
x GRID_SIZE
kare sığdırabilmek için karelerinizi nasıl oluşturduğunuzu güncellemeniz gerekir. Bu nedenle, karenin çok daha küçük olması ve çok sayıda kare olması gerekir.
Bu soruna yaklaşabileceğiniz bir yöntem, köşe arabelleğinizi önemli ölçüde büyütmek ve içinde doğru boyut ve konumda GRID_SIZE
kez GRID_SIZE
kare tanımlamaktır. Bunun için kod yazmak aslında çok da zor olmaz. Yalnızca birkaç for döngüsü ve biraz matematik. Ancak bu yöntem, GPU'nun en iyi şekilde kullanılmasını sağlamaz ve efektin elde edilmesi için gerekenden daha fazla bellek kullanır. Bu bölümde, GPU'ya daha uygun bir yaklaşım ele alınmaktadır.
Tekdüzen arabellek oluşturma
Öncelikle, öğelerin nasıl görüntüleneceğini değiştirmek için kullanılan gölgelendiriciye seçtiğiniz ızgara boyutunu bildirmeniz gerekir. Boyutu doğrudan gölgelendiriciye kodlayabilirsiniz ancak bu durumda, ızgara boyutunu değiştirmek istediğiniz her seferde gölgelendiriciyi ve oluşturma işlem hattını yeniden oluşturmanız gerekir. Bu da maliyetli bir işlemdir. Daha iyi bir yöntem, ızgara boyutunu gölgelendiriciye uniforms olarak sağlamaktır.
Daha önce, köşe arabelleğindeki farklı bir değerin köşe gölgelendiricinin her çağrılmasına iletildiğini öğrenmiştiniz. Tekdüzen, her çağırmada aynı olan bir arabellek değeridir. Bir geometri parçası (ör. konumu), animasyonun tamamı (ör. geçerli saat) veya uygulamanın tüm kullanım ömrü (ör. kullanıcı tercihi) için ortak olan değerleri iletmek amacıyla kullanılır.
- Aşağıdaki kodu ekleyerek tek tip bir arabellek oluşturun:
index.html
// Create a uniform buffer that describes the grid.
const uniformArray = new Float32Array([GRID_SIZE, GRID_SIZE]);
const uniformBuffer = device.createBuffer({
label: "Grid Uniforms",
size: uniformArray.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(uniformBuffer, 0, uniformArray);
Bu kod, daha önce köşe arabelleğini oluşturmak için kullandığınız kodla neredeyse tamamen aynı olduğundan çok tanıdık gelecektir. Bunun nedeni, tek tip değişkenlerin WebGPU API'ye, köşe noktalarıyla aynı GPUBuffer nesneleri aracılığıyla iletilmesidir. Aradaki temel fark, bu kez usage
öğesinin GPUBufferUsage.VERTEX
yerine GPUBufferUsage.UNIFORM
içermesidir.
Bir gölgelendiricide tek tip değişkenlere erişme
- Aşağıdaki kodu ekleyerek bir forma tanımlayın:
index.html (createShaderModule çağrısı)
// At the top of the `code` string in the createShaderModule() call
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
return vec4f(pos / grid, 0, 1);
}
// ...fragmentMain is unchanged
Bu, gölgelendiricinizde grid
adlı bir tekdüzen tanımlar. Bu, tekdüzen arabelleğe kopyaladığınız diziyle eşleşen 2 boyutlu bir kayan nokta vektörüdür. Ayrıca üniformanın @group(0)
ve @binding(0)
konumlarında bağlı olduğunu belirtir. Bu değerlerin ne anlama geldiğini birazdan öğreneceksiniz.
Ardından, gölgelendirici kodunun başka bir yerinde ızgara vektörünü istediğiniz gibi kullanabilirsiniz. Bu kodda, köşe konumunu ızgara vektörüne bölersiniz. pos
2 boyutlu bir vektör ve grid
2 boyutlu bir vektör olduğundan WGSL, bileşen bazında bölme işlemi gerçekleştirir. Başka bir deyişle, sonuç vec2f(pos.x / grid.x, pos.y / grid.y)
demeyle aynıdır.
Bu tür vektör işlemleri, birçok oluşturma ve hesaplama tekniği bunlara dayandığından GPU gölgelendiricilerinde çok yaygındır.
Bu durumda (4 birimlik bir ızgara boyutu kullandıysanız) oluşturduğunuz karenin orijinal boyutunun dörtte biri olacağı anlamına gelir. Dört tanesini bir satıra veya sütuna sığdırmak istiyorsanız bu boyut idealdir.
Bağlama grubu oluşturma
Ancak, gölgelendiricideki tekdüzen değişkeni bildirmek, onu oluşturduğunuz arabelleğe bağlamaz. Bunun için bağlama grubu oluşturup ayarlamanız gerekir.
Bağlama grubu, gölgelendiricinize aynı anda erişilebilir kılmak istediğiniz kaynakların bir koleksiyonudur. Bu, tek tip arabellek gibi çeşitli arabellek türlerini ve burada ele alınmayan ancak WebGPU oluşturma tekniklerinin yaygın parçaları olan dokular ve örnekleyiciler gibi diğer kaynakları içerebilir.
- Aşağıdaki kodu, tekdüzen arabellek ve oluşturma ardışık düzeni oluşturulduktan sonra ekleyerek tekdüzen arabelleğinizle bir bağlama grubu oluşturun:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}],
});
Artık standart olan label
'a ek olarak, bu bağlama grubunun hangi kaynak türlerini içerdiğini açıklayan bir layout
de gerekir. Bu, gelecekteki bir adımda daha ayrıntılı olarak inceleyeceğiniz bir konudur ancak şu an için layout: "auto"
ile işlem hattını oluşturduğunuzdan işlem hattınızdan bağlama grubu düzenini isteyebilirsiniz. Bu, işlem hattının, bağlayıcı kodun kendisinde bildirdiğiniz bağlamalardan otomatik olarak bağlama grubu düzenleri oluşturmasına neden olur. Bu durumda, getBindGroupLayout(0)
yazarak 0
değerini, gölgelendiricide yazdığınız @group(0)
değerine karşılık gelecek şekilde ayarlarsınız.
Düzeni belirttikten sonra entries
dizisi sağlarsınız. Her giriş, en az aşağıdaki değerleri içeren bir sözlüktür:
binding
, gölgelendiricide girdiğiniz@binding()
değeriyle eşleşir. Bu durumda,0
.- Belirtilen bağlama dizinindeki değişkene göstermek istediğiniz gerçek kaynak olan
resource
. Bu durumda, tek tip arabelleğiniz.
İşlev, opak ve değişmez bir tutma yeri olan GPUBindGroup
döndürür. Bağlama grubu oluşturulduktan sonra, bu grubun işaret ettiği kaynakları değiştiremezsiniz ancak bu kaynakların içeriğini değiştirebilirsiniz. Örneğin, tek tip arabelleği yeni bir ızgara boyutu içerecek şekilde değiştirirseniz bu bağlama grubunu kullanan gelecekteki çizim çağrıları bu değişikliği yansıtır.
Bağlama grubunu bağlama
Bağlama grubu oluşturulduktan sonra, çizim yaparken WebGPU'ya bu grubu kullanmasını söylemeniz gerekir. Neyse ki bu işlem oldukça basittir.
- Render geçişine geri dönün ve
draw()
yönteminden önce bu yeni satırı ekleyin:
index.html
pass.setPipeline(cellPipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup); // New line!
pass.draw(vertices.length / 2);
İlk bağımsız değişken olarak iletilen 0
, gölgelendirici kodundaki @group(0)
değerine karşılık gelir. @group(0)
içinde yer alan her @binding
, bu bağlama grubundaki kaynakları kullanıyor.
Artık tek tip arabellek, gölgelendiricinize sunuluyor.
- Sayfanızı yenilediğinizde aşağıdakine benzer bir ekranla karşılaşırsınız:
Yaşasın! Kare artık önceki boyutunun dörtte biri kadar. Bu çok fazla olmasa da tekdüzeliğinizin gerçekten uygulandığını ve gölgelendiricinin artık ızgaranızın boyutuna erişebildiğini gösterir.
Gölgelendiricide geometriyi değiştirme
Artık gölgelendiricide ızgara boyutuna başvurabildiğinize göre, oluşturduğunuz geometriyi istediğiniz ızgara desenine uyacak şekilde değiştirmek için çalışmaya başlayabilirsiniz. Bunu yapmak için tam olarak neyi başarmak istediğinizi düşünün.
Tuvalinizi kavramsal olarak ayrı hücrelere bölmeniz gerekir. X ekseninin sağa doğru gidildikçe, Y ekseninin ise yukarı doğru gidildikçe arttığı kuralını korumak için ilk hücrenin tuvalin sol alt köşesinde olduğunu varsayalım. Bu işlem, mevcut kare geometrinizin ortada olduğu aşağıdaki gibi bir düzen oluşturur:
Buradaki zorluk, hücre koordinatları verildiğinde kare geometrisini bu hücrelerin herhangi birine yerleştirmenizi sağlayan bir yöntemi gölgelendiricide bulmaktır.
Öncelikle, tuvalin merkezini çevreleyecek şekilde tanımlandığı için karenizin hücrelerle düzgün şekilde hizalanmadığını görebilirsiniz. Kareyi, hücrelerin içinde düzgün bir şekilde hizalanacak şekilde yarım hücre kaydırmak isteyebilirsiniz.
Bu sorunu düzeltmenin bir yolu, karenin köşe arabelleğini güncellemektir. Köşeleri, sol alt köşesi örneğin (-0.8, -0.8) yerine (0.1, 0.1) olacak şekilde kaydırarak bu kareyi hücre sınırlarıyla daha iyi hizalayabilirsiniz. Ancak, köşe noktalarının gölgelendiricinizde nasıl işleneceği üzerinde tam kontrole sahip olduğunuz için gölgelendirici kodunu kullanarak bunları yerine yerleştirmek de aynı derecede kolaydır.
- Köşe gölgelendirici modülünü aşağıdaki kodla değiştirin:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Add 1 to the position before dividing by the grid size.
let gridPos = (pos + 1) / grid;
return vec4f(gridPos, 0, 1);
}
Bu işlem, her köşeyi ızgara boyutuyla bölmeden önce bir birim yukarı ve sağa taşır (bir birimin, kırpma alanının yarısı olduğunu unutmayın). Sonuç, başlangıç noktasının hemen dışında, ızgarayla düzgün şekilde hizalanmış bir karedir.
Ardından, tuvalinizin koordinat sistemi (0, 0) konumunu merkeze ve (-1, -1) konumunu sol alta yerleştirdiğinden ve (0, 0) konumunun sol altta olmasını istediğinizden, geometrinizin konumunu bu köşeye taşımak için ızgara boyutuna böldükten sonra (-1, -1) ile çevirmeniz gerekir.
- Geometrinizin konumunu aşağıdaki gibi çevirin:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
// Subtract 1 after dividing by the grid size.
let gridPos = (pos + 1) / grid - 1;
return vec4f(gridPos, 0, 1);
}
Artık kareniz (0, 0) hücresinde düzgün bir şekilde konumlandırılmış durumda.
Farklı bir hücreye yerleştirmek isterseniz ne yapmanız gerekir? Bunu, gölgelendiricinizde bir cell
vektörü bildirerek ve bunu let cell = vec2f(1, 1)
gibi statik bir değerle doldurarak belirleyin.
Bunu gridPos
öğesine eklerseniz algoritmadaki - 1
işlemi geri alınır. Bu nedenle, bunu yapmamanız gerekir. Bunun yerine, kareyi her hücre için yalnızca bir ızgara birimi (tuvalin dörtte biri) kadar taşımak istiyorsunuz. grid
ile bir kez daha bölmeniz gerekiyor.
- Izgara konumlandırmanızı aşağıdaki gibi değiştirin:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1); // Cell(1,1) in the image above
let cellOffset = cell / grid; // Compute the offset to cell
let gridPos = (pos + 1) / grid - 1 + cellOffset; // Add it here!
return vec4f(gridPos, 0, 1);
}
Şimdi yenilerseniz aşağıdakileri görürsünüz:
Hm. İstediğiniz gibi değil mi?
Bunun nedeni, tuval koordinatları -1 ile +1 arasında değiştiği için gerçekte 2 birim genişliğinde olmasıdır. Yani bir tepe noktasını tuvalin dörtte biri kadar taşımak istiyorsanız 0, 5 birim taşımanız gerekir. GPU koordinatlarıyla akıl yürütürken bu hatayı yapmak kolaydır. Neyse ki bu sorunu düzeltmek de aynı derecede kolay.
- Telafinizi 2 ile çarpın:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f) ->
@builtin(position) vec4f {
let cell = vec2f(1, 1);
let cellOffset = cell / grid * 2; // Updated
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Bu da size tam olarak istediğinizi verir.
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şturulmuş olarak görmek için yenileyebilirsiniz.
Çizim örnekleri
Kareyi biraz matematik yardımıyla istediğiniz yere yerleştirebildiğinize göre, bir sonraki adımda ızgaranın her hücresinde bir kare oluşturmanız gerekir.
Bu soruna yaklaşmanın bir yolu, hücre koordinatlarını tek tip bir arabelleğe yazmak, ardından ızgaradaki her kare için draw işlevini bir kez çağırmak ve tek tipi her seferinde güncellemek olabilir. Ancak GPU'nun her seferinde yeni koordinatın JavaScript tarafından yazılmasını beklemesi gerektiğinden bu işlem çok yavaş olur. GPU'dan iyi performans elde etmenin anahtarlarından biri, sistemin diğer kısımlarını beklerken harcadığı süreyi en aza indirmektir.
Bunun yerine, örnekleme adı verilen bir teknik kullanabilirsiniz. Örnekleme, GPU'ya aynı geometrinin birden fazla kopyasını tek bir draw
çağrısıyla çizmesini söylemenin bir yoludur. Bu, her kopya için draw
'yı bir kez çağırmaktan çok daha hızlıdır. Geometrinin her kopyasına örnek adı verilir.
- GPU'ya, ızgarayı dolduracak kadar kare örneği istediğinizi söylemek için mevcut çizim çağrınıza bir bağımsız değişken ekleyin:
index.html
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
Bu, sisteme karenizin altı (vertices.length / 2
) köşesini 16 (GRID_SIZE * GRID_SIZE
) kez çizmesini istediğinizi söyler. Ancak sayfayı yenilediğinizde aşağıdaki mesajı görmeye devam ediyorsanız:
Neden? Bunun nedeni, 16 karenin tamamını aynı noktaya çizmenizdir. Gölgelendiricide, geometriyi örnek bazında yeniden konumlandıran bazı ek mantıklar olması gerekir.
Shader'da, köşe arabelleğinizden gelen pos
gibi köşe özelliklerinin yanı sıra WGSL'nin yerleşik değerleri olarak bilinenlere de erişebilirsiniz. Bunlar WebGPU tarafından hesaplanan değerlerdir ve bu değerlerden biri instance_index
'dır. instance_index
, gölgelendirici mantığınızın bir parçası olarak kullanabileceğiniz, 0
ile number of instances - 1
arasındaki işaretsiz 32 bitlik bir sayıdır. Değeri, aynı örneğin parçası olan her işlenmiş köşe için aynıdır. Bu durumda, köşe arabelleğinizdeki her konum için bir kez olmak üzere, köşe gölgelendiriciniz instance_index
değeri 0
olan altı kez çağrılır. Ardından instance_index
1
ile altı kez daha, sonra instance_index
2
ile altı kez daha ve bu şekilde devam eder.
Bunu uygulamada görmek için instance_index
yerleşik işlevini gölgelendirici girişlerinize eklemeniz gerekir. Bu işlemi konumla aynı şekilde yapın ancak @location
özelliğiyle etiketlemek yerine @builtin(instance_index)
özelliğini kullanın ve ardından bağımsız değişkene istediğiniz adı verin. (Örnek kodla eşleşmesi için instance
olarak adlandırabilirsiniz.) Ardından, bunu gölgelendirici mantığının bir parçası olarak kullanın.
- Hücre koordinatları yerine
instance
kullanın:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance); // Save the instance_index as a float
let cell = vec2f(i, i); // Updated
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Şimdi yenilerseniz gerçekten birden fazla kare olduğunu görürsünüz. Ancak 16 öğenin tamamını göremezsiniz.
Bunun nedeni, oluşturduğunuz hücre koordinatlarının (0, 0), (1, 1), (2, 2)... (15, 15) şeklinde olması ancak bunlardan yalnızca ilk dördünün tuval üzerine sığmasıdır. İstediğiniz ızgarayı oluşturmak için instance_index
öğesini, her dizinin ızgaranızdaki benzersiz bir hücreyle eşleneceği şekilde dönüştürmeniz gerekir. Örneğin:
Bunun hesabı oldukça basittir. Her hücrenin X değeri için instance_index
ve ızgara genişliğinin modülünü istiyorsunuz. Bunu %
operatörüyle WGSL'de yapabilirsiniz. Ayrıca, her hücrenin Y değeri için instance_index
değerini ızgara genişliğine bölmek ve kesirli kalanları atmak istersiniz. Bu işlemi WGSL'nin floor()
işleviyle yapabilirsiniz.
- Hesaplamaları aşağıdaki gibi değiştirin:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
// Compute the cell coordinate from the instance_index
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
Kodu güncelledikten sonra nihayet uzun zamandır beklediğiniz kareler ızgarasına sahip olursunuz.
- Şimdi çalışıyor olduğuna göre geri dönüp ızgara boyutunu artırabilirsiniz.
index.html
const GRID_SIZE = 32;
Tada! Bu kılavuzu artık çok büyük hale getirebilirsiniz ve ortalama bir GPU bunu sorunsuz bir şekilde işleyebilir. GPU performansıyla ilgili darboğazlarla karşılaşmadan çok önce ayrı kareleri görmeyi bırakırsınız.
6. Ekstra kredi: Daha renkli hale getirin.
Bu noktada, codelab'in geri kalanı için temel bilgileri edindiğinizden bir sonraki bölüme kolayca geçebilirsiniz. Ancak aynı rengi paylaşan karelerden oluşan ızgara işe yarasa da pek heyecan verici değil, değil mi? Neyse ki biraz daha matematik ve gölgelendirici koduyla işleri biraz daha aydınlatabilirsiniz.
Shader'larda yapıları kullanma
Şimdiye kadar, köşe gölgelendiriciden bir veri parçası geçirdiniz: dönüştürülmüş konum. Ancak, köşe gölgelendiricisinden çok daha fazla veri döndürebilir ve bunları parça gölgelendiricide kullanabilirsiniz.
Verileri köşe gölgelendiriciden geçirmenin tek yolu döndürmektir. Bir köşe gölgelendiricinin her zaman bir konum döndürmesi gerekir. Bu nedenle, konumla birlikte başka veriler de döndürmek istiyorsanız bu verileri bir yapının içine yerleştirmeniz gerekir. WGSL'deki yapılar, bir veya daha fazla adlandırılmış özellik içeren adlandırılmış nesne türleridir. Mülkler, @builtin
ve @location
gibi özelliklerle de işaretlenebilir. Bunları işlevlerin dışında tanımlarsınız ve gerektiğinde işlevlere iletip işlevlerden çıkarabilirsiniz. Örneğin, mevcut köşe gölgelendiricinizi düşünün:
index.html (createShaderModule çağrısı)
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) ->
@builtin(position) vec4f {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (pos + 1) / grid - 1 + cellOffset;
return vec4f(gridPos, 0, 1);
}
- Aynı şeyi, işlev girişi ve çıkışı için yapılar kullanarak ifade edin:
index.html (createShaderModule çağrısı)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
return output;
}
Bunun için giriş konumuna ve örnek dizinine input
ile başvurmanız gerektiğini ve önce döndürdüğünüz yapının değişken olarak bildirilmesi ve bağımsız özelliklerinin ayarlanması gerektiğini unutmayın. Bu durumda, çok fazla fark yaratmaz ve hatta gölgelendirici işlevini biraz daha uzun hale getirir. Ancak gölgelendiricileriniz daha karmaşık hale geldikçe yapıları kullanmak, verilerinizi düzenlemenize yardımcı olabilecek harika bir yol olabilir.
Köşe ve parça işlevleri arasında veri aktarma
@fragment
işlevinin olabildiğince basit olduğunu hatırlatmak isteriz:
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
Giriş almıyor ve çıkış olarak düz bir renk (kırmızı) veriyorsunuz. Ancak gölgelendirici, renklendirdiği geometri hakkında daha fazla bilgiye sahip olsaydı bu ek verileri kullanarak işleri biraz daha ilginç hale getirebilirdiniz. Örneğin, her karenin rengini hücre koordinatına göre değiştirmek istiyorsanız ne yapmanız gerekir? @vertex
aşaması, hangi hücrenin oluşturulduğunu bilir. Bunu @fragment
aşamasına iletmeniz yeterlidir.
Köşe ve parça aşamaları arasında herhangi bir veriyi iletmek için bu veriyi, bizim seçtiğimiz bir @location
ile çıkış yapısında eklemeniz gerekir. Hücre koordinatını iletmek istediğiniz için bunu daha önce oluşturduğunuz VertexOutput
yapısına ekleyin ve döndürmeden önce @vertex
işlevinde ayarlayın.
- Köşe gölgelendiricinizin dönüş değerini aşağıdaki gibi değiştirin:
index.html (createShaderModule çağrısı)
struct VertexInput {
@location(0) pos: vec2f,
@builtin(instance_index) instance: u32,
};
struct VertexOutput {
@builtin(position) pos: vec4f,
@location(0) cell: vec2f, // New line!
};
@group(0) @binding(0) var<uniform> grid: vec2f;
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput {
let i = f32(input.instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let cellOffset = cell / grid * 2;
let gridPos = (input.pos + 1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell; // New line!
return output;
}
@fragment
işlevinde, aynı@location
ile bir bağımsız değişken ekleyerek değeri alın. (Adların eşleşmesi gerekmez ancak eşleşirse işleri takip etmek daha kolay olur.)
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain(@location(0) cell: vec2f) -> @location(0) vec4f {
// Remember, fragment return values are (Red, Green, Blue, Alpha)
// and since cell is a 2D vector, this is equivalent to:
// (Red = cell.x, Green = cell.y, Blue = 0, Alpha = 1)
return vec4f(cell, 0, 1);
}
- Alternatif olarak, bunun yerine bir yapı da kullanabilirsiniz:
index.html (createShaderModule çağrısı)
struct FragInput {
@location(0) cell: vec2f,
};
@fragment
fn fragmentMain(input: FragInput) -> @location(0) vec4f {
return vec4f(input.cell, 0, 1);
}
- Kodunuzda bu işlevlerin her ikisi de aynı gölgelendirici modülünde tanımlandığından,
@vertex
aşamasının çıkış yapısını yeniden kullanmak da bir alternatiftir. Adlar ve konumlar doğal olarak tutarlı olduğundan bu yöntem, değerlerin iletilmesini kolaylaştırır.
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.cell, 0, 1);
}
Hangi deseni seçerseniz seçin, sonuç olarak @fragment
işlevinde hücre numarasına erişebilir ve rengi etkilemek için bu numarayı kullanabilirsiniz. Yukarıdaki kodlardan herhangi biriyle çıkış şu şekilde görünür:
Şimdi kesinlikle daha fazla renk var ancak görünümü pek hoş değil. Yalnızca sol ve alt satırların neden farklı olduğunu merak edebilirsiniz. Bunun nedeni, @fragment
işlevinden döndürdüğünüz renk değerlerinin her kanalın 0 ile 1 aralığında olmasını beklemesi ve bu aralığın dışındaki değerlerin bu aralığa sabitlenmesidir. Hücre değerleriniz ise her eksende 0 ile 32 arasında değişir. Burada gördüğünüz gibi, ilk satır ve sütun kırmızı veya yeşil renk kanalında hemen 1 değerine ulaşıyor ve bundan sonraki her hücre aynı değere sabitleniyor.
Renkler arasında daha yumuşak bir geçiş istiyorsanız her renk kanalı için kesirli bir değer döndürmeniz gerekir. İdeal olarak her eksende sıfırla başlayıp birle biten bu değerler, grid
ile bir kez daha bölme işlemi yapmanız gerektiği anlamına gelir.
- Parça gölgelendiricisini şu şekilde değiştirin:
index.html (createShaderModule çağrısı)
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.cell/grid, 0, 1);
}
Sayfayı yenilediğinizde yeni kodun, ızgaranın tamamında çok daha güzel bir renk geçişi sağladığını görebilirsiniz.
Bu kesinlikle bir iyileştirme olsa da artık sol altta, ızgaranın siyahlaştığı talihsiz bir karanlık köşe var. Hayat Oyunu simülasyonunu yapmaya başladığınızda, ızgaranın zor görünen bir bölümü olanları gizler. It would be nice to brighten that up.
Neyse ki kullanabileceğiniz, kullanılmamış bir renk kanalı (mavi) var. İdeal olarak, diğer renklerin en koyu olduğu yerlerde mavinin en parlak olmasını ve diğer renklerin yoğunluğu arttıkça mavinin solmasını istersiniz. Bunu yapmanın en kolay yolu, kanalın 1'den başlamasını sağlamak ve hücre değerlerinden birini çıkarmaktır. c.x
veya c.y
olabilir. İkisini de deneyip tercih ettiğinizi seçin.
- Parça gölgelendiriciye daha parlak renkler ekleyin (ör. aşağıdaki gibi):
createShaderModule çağrısı
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
let c = input.cell / grid;
return vec4f(c, 1-c.x, 1);
}
Sonuç oldukça güzel görünüyor.
Bu kritik bir adım değildir. Ancak daha iyi göründüğü için ilgili kontrol noktası kaynak dosyasına dahil edildi ve bu codelab'deki ekran görüntülerinin geri kalanında bu daha renkli ızgara yansıtıldı.
7. Hücre durumunu yönetme
Ardından, GPU'da depolanan bazı durumlara göre ızgaradaki hangi hücrelerin oluşturulacağını kontrol etmeniz gerekir. Bu, son simülasyon için önemlidir.
Tek ihtiyacınız her hücre için bir açma/kapama sinyalidir. Bu nedenle, neredeyse her değer türünden oluşan büyük bir dizi depolamanıza olanak tanıyan tüm seçenekler işe yarar. Bunun, tek tip arabelleklerin bir başka kullanım alanı olduğunu düşünebilirsiniz. Bu işi yapabilirsiniz ancak tek tip arabelleklerin boyutu sınırlı olduğundan, dinamik olarak boyutlandırılmış dizileri desteklemediğinden (dizi boyutunu gölgelendiricide belirtmeniz gerekir) ve hesaplama gölgelendiricileri tarafından yazılamadığından bu daha zordur. Son öğe, Yaşam Oyunu simülasyonunu bir hesaplama gölgelendiricisinde GPU'da yapmak istediğiniz için en sorunlu olanıdır.
Neyse ki tüm bu sınırlamaları ortadan kaldıran başka bir arabellek seçeneği var.
Depolama arabelleği oluşturma
Depolama arabellekleri, işlem gölgelendiricilerinde okunup yazılabilen ve tepe gölgelendiricilerinde okunabilen genel amaçlı arabelleklerdir. Çok büyük olabilirler ve bir gölgelendiricide belirli bir boyut bildirilmesi gerekmez. Bu da onları genel belleğe çok daha fazla benzetir. Hücre durumunu depolamak için bunu kullanırsınız.
- Hücre durumunuz için bir depolama arabelleği oluşturmak üzere, muhtemelen artık tanıdık görünmeye başlayan bir arabellek oluşturma kodu snippet'i kullanın:
index.html
// Create an array representing the active state of each cell.
const cellStateArray = new Uint32Array(GRID_SIZE * GRID_SIZE);
// Create a storage buffer to hold the cell state.
const cellStateStorage = device.createBuffer({
label: "Cell State",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
Köşe ve tekdüze arabelleklerinizde olduğu gibi, device.createBuffer()
işlevini uygun boyutta çağırın ve bu kez GPUBufferUsage.STORAGE
kullanımını belirttiğinizden emin olun.
Aynı boyuttaki TypedArray'i değerlerle doldurup device.queue.writeBuffer()
işlevini çağırarak arabelleği eskisi gibi doldurabilirsiniz. Arabelleğinizin ızgara üzerindeki etkisini görmek istediğiniz için arabelleği tahmin edilebilir bir şeyle doldurarak başlayın.
- Aşağıdaki kodu kullanarak her üçüncü hücreyi etkinleştirin:
index.html
// Mark every third cell of the grid as active.
for (let i = 0; i < cellStateArray.length; i += 3) {
cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage, 0, cellStateArray);
Gölgelendiricide depolama arabelleğini okuma
Ardından, ızgarayı oluşturmadan önce depolama arabelleğinin içeriğine bakmak için gölgelendiricinizi güncelleyin. Bu, daha önce üniformaların eklenme şekline çok benzer.
- Gölgelendiricinizi aşağıdaki kodla güncelleyin:
index.html
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellState: array<u32>; // New!
Öncelikle, bağlama noktasını ekleyin. Bu nokta, ızgara üniformasının hemen altına yerleştirilir. @group
ile grid
forması aynı olsun istiyorsunuz ancak @binding
numarası farklı olmalı. Farklı arabellek türünü yansıtmak için var
türü storage
'dir ve cellState
için verdiğiniz tür, JavaScript'teki Uint32Array
ile eşleşmesi için tek bir vektör yerine u32
değerlerinden oluşan bir dizidir.
Ardından, @vertex
işlevinizin gövdesinde hücrenin durumunu sorgulayın. Durum, depolama arabelleğinde düz bir dizide depolandığından geçerli hücrenin değerini aramak için instance_index
kullanabilirsiniz.
Eyalet, hücrenin etkin olmadığını söylüyorsa hücreyi nasıl kapatırsınız? Diziden aldığınız etkin ve etkin olmayan durumlar 1 veya 0 olduğundan, geometriyi etkin duruma göre ölçeklendirebilirsiniz. 1 ile ölçeklendirme geometride değişiklik yapmazken 0 ile ölçeklendirme geometrinin tek bir noktaya daralmasına neden olur. Bu nokta daha sonra GPU tarafından atılır.
- Konumu hücrenin etkin durumuna göre ölçeklendirmek için gölgelendirici kodunuzu güncelleyin. WGSL'nin tür güvenliği şartlarını karşılamak için durum değeri
f32
olarak yayınlanmalıdır:
index.html
@vertex
fn vertexMain(@location(0) pos: vec2f,
@builtin(instance_index) instance: u32) -> VertexOutput {
let i = f32(instance);
let cell = vec2f(i % grid.x, floor(i / grid.x));
let state = f32(cellState[instance]); // New line!
let cellOffset = cell / grid * 2;
// New: Scale the position by the cell's active state.
let gridPos = (pos*state+1) / grid - 1 + cellOffset;
var output: VertexOutput;
output.pos = vec4f(gridPos, 0, 1);
output.cell = cell;
return output;
}
Depolama arabelleğini bağlama grubuna ekleme
Hücre durumunun etkisini görebilmek için depolama arabelleğini bir bağlama grubuna ekleyin. Tekdüzen arabellek ile aynı @group
'nın parçası olduğundan, JavaScript kodunda da aynı bağlama grubuna ekleyin.
- Depolama tamponunu aşağıdaki gibi ekleyin:
index.html
const bindGroup = device.createBindGroup({
label: "Cell renderer bind group",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
},
// New entry!
{
binding: 1,
resource: { buffer: cellStateStorage }
}],
});
Yeni girişin binding
değerinin, gölgelendiricideki ilgili değerin @binding()
değeriyle eşleştiğinden emin olun.
Bu işlemden sonra yenileyebilir ve kalıbın kılavuzda görünmesini sağlayabilirsiniz.
Ping-pong arabellek düzenini kullanma
Oluşturduğunuz gibi çoğu simülasyon genellikle durumunun en az iki kopyasını kullanır. Simülasyonun her adımında, durumun bir kopyasından okuma yapıp diğerine yazma işlemi gerçekleştirirler. Ardından, bir sonraki adımda kağıdı çevirin ve daha önce yazdıkları durumdan okumaya devam edin. Bu durum, durumun en güncel sürümü her adımda durum kopyaları arasında ileri geri hareket ettiğinden genellikle ping pong (masa tenisi) modeli olarak adlandırılır.
Bu neden gerekli? Basitleştirilmiş bir örneğe bakalım: Her adımda etkin blokları bir hücre sağa taşıdığınız çok basit bir simülasyon yazdığınızı düşünün. Kolay anlaşılması için verilerinizi ve simülasyonunuzu JavaScript'te tanımlarsınız:
// Example simulation. Don't copy into the project!
const state = [1, 0, 0, 0, 0, 0, 0, 0];
function simulate() {
for (let i = 0; i < state.length-1; ++i) {
if (state[i] == 1) {
state[i] = 0;
state[i+1] = 1;
}
}
}
simulate(); // Run the simulation for one step.
Ancak bu kodu çalıştırırsanız etkin hücre, tek bir adımda dizinin sonuna kadar gider. Neden? Çünkü durumu yerinde güncellemeye devam ediyorsunuz. Bu nedenle, etkin hücreyi sağa taşıyıp sonraki hücreye bakıyorsunuz ve... hey! Etkinleştirildi. Doğru yere tekrar taşıyın. Verileri gözlemlerken aynı anda değiştirmeniz sonuçları bozar.
Ping pong modelini kullanarak simülasyonun bir sonraki adımını her zaman yalnızca son adımın sonuçlarını kullanarak gerçekleştirebilirsiniz.
// Example simulation. Don't copy into the project!
const stateA = [1, 0, 0, 0, 0, 0, 0, 0];
const stateB = [0, 0, 0, 0, 0, 0, 0, 0];
function simulate(inArray, outArray) {
outArray[0] = 0;
for (let i = 1; i < inArray.length; ++i) {
outArray[i] = inArray[i-1];
}
}
// Run the simulation for two step.
simulate(stateA, stateB);
simulate(stateB, stateA);
- İki özdeş arabellek oluşturmak için depolama arabelleği ayırmanızı güncelleyerek bu kalıbı kendi kodunuzda kullanın:
index.html
// Create two storage buffers to hold the cell state.
const cellStateStorage = [
device.createBuffer({
label: "Cell State A",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
}),
device.createBuffer({
label: "Cell State B",
size: cellStateArray.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})
];
- İki arabellek arasındaki farkı görselleştirmek için arabellekleri farklı verilerle doldurun:
index.html
// Mark every third cell of the first grid as active.
for (let i = 0; i < cellStateArray.length; i+=3) {
cellStateArray[i] = 1;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
// Mark every other cell of the second grid as active.
for (let i = 0; i < cellStateArray.length; i++) {
cellStateArray[i] = i % 2;
}
device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray);
- Render'ınızda farklı depolama arabelleklerini göstermek için bağlama gruplarınızı iki farklı varyanta sahip olacak şekilde güncelleyin:
index.html
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: cellPipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}],
})
];
Render döngüsü oluşturma
Şimdiye kadar sayfa yenileme başına yalnızca bir çekim yaptınız ancak artık zaman içinde güncellenen verileri göstermek istiyorsunuz. Bunun için basit bir oluşturma döngüsüne ihtiyacınız vardır.
Render döngüsü, içeriğinizi belirli aralıklarla tuvale çizen, sonsuza kadar tekrarlanan bir döngüdür. Sorunsuz animasyonlar kullanmak isteyen birçok oyun ve diğer içerikler, geri çağırmaları ekranın yenilenme hızıyla aynı oranda (saniyede 60 kez) planlamak için requestAnimationFrame()
işlevini kullanır.
Bu uygulama da bunu kullanabilir ancak bu durumda, simülasyonun ne yaptığını daha kolay takip edebilmek için güncellemelerin daha uzun adımlarla yapılmasını isteyebilirsiniz. Simülasyonunuzun güncellenme hızını kontrol edebilmek için döngüyü kendiniz yönetin.
- Öncelikle simülasyonumuzun güncelleneceği bir hız seçin (200 ms iyi bir hızdır ancak isterseniz daha yavaş veya daha hızlı da gidebilirsiniz). Ardından, tamamlanan simülasyon adımlarının sayısını takip edin.
index.html
const UPDATE_INTERVAL = 200; // Update every 200ms (5 times/sec)
let step = 0; // Track how many simulation steps have been run
- Ardından, oluşturma için kullandığınız tüm kodu yeni bir işleve taşıyın. Bu işlevi
setInterval()
ile istediğiniz aralıkta tekrarlanacak şekilde planlayın. İşlevin adım sayısını da güncellediğinden emin olun ve hangi bağlama grubunun bağlanacağını seçmek için bu bilgiyi kullanın.
index.html
// Move all of our rendering code into a function
function updateGrid() {
step++; // Increment the step count
// Start a render pass
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: "clear",
clearValue: { r: 0, g: 0, b: 0.4, a: 1.0 },
storeOp: "store",
}]
});
// Draw the grid.
pass.setPipeline(cellPipeline);
pass.setBindGroup(0, bindGroups[step % 2]); // Updated!
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(vertices.length / 2, GRID_SIZE * GRID_SIZE);
// End the render pass and submit the command buffer
pass.end();
device.queue.submit([encoder.finish()]);
}
// Schedule updateGrid() to run repeatedly
setInterval(updateGrid, UPDATE_INTERVAL);
Uygulamayı çalıştırdığınızda tuvalin, oluşturduğunuz iki durum arabelleğini göstererek ileri geri döndüğünü görürsünüz.
Böylece, oluşturma tarafındaki işlemlerinizi tamamlamış olursunuz. Bir sonraki adımda, nihayet hesaplama gölgelendiricilerini kullanmaya başlayacağınız Yaşam Oyunu simülasyonunun çıktısını göstermeye hazırsınız.
WebGPU'nun oluşturma özelliklerinin, burada incelediğiniz küçük bölümden çok daha fazlasını içerdiği açıktır ancak geri kalan kısım bu kod laboratuvarının kapsamı dışındadır. Bununla birlikte, WebGPU'nun oluşturma işleminin nasıl çalıştığına dair yeterli bir fikir vereceğini ve 3D oluşturma gibi daha gelişmiş teknikleri anlamayı kolaylaştıracağını umuyoruz.
8. Simülasyonu çalıştırma
Şimdi de bulmacanın son büyük parçasına geçelim: Hayat Oyunu simülasyonunu bir hesaplama gölgelendiricisinde gerçekleştirme.
Sonunda hesaplama gölgelendiricilerini kullanabilirsiniz!
Bu codelab boyunca işlem gölgelendiricileri hakkında soyut bilgiler edindiniz. Peki bunlar tam olarak nedir?
İşlem gölgelendiriciler, GPU'da son derece paralel çalışacak şekilde tasarlanmaları bakımından köşe ve parça gölgelendiricilere benzer. Ancak diğer iki gölgelendirici aşamasının aksine, belirli bir giriş ve çıkış kümesi yoktur. Verileri yalnızca seçtiğiniz kaynaklardan (ör. depolama arabellekleri) okuyup yazarsınız. Bu, her bir köşe, örnek veya piksel için bir kez yürütmek yerine gölgelendirici işlevinin kaç kez çağrılmasını istediğinizi belirtmeniz gerektiği anlamına gelir. Ardından, gölgelendiriciyi çalıştırdığınızda hangi çağırmanın işlendiği size bildirilir ve buradan hangi verilere erişeceğinize ve hangi işlemleri yapacağınıza karar verebilirsiniz.
Compute shader'lar, vertex ve fragment shader'lar gibi bir shader modülünde oluşturulmalıdır. Bu nedenle, başlamak için bunu kodunuza ekleyin. Uyguladığınız diğer gölgelendiricilerin yapısı göz önüne alındığında, hesaplama gölgelendiricinizin ana işlevinin @compute
özelliğiyle işaretlenmesi gerektiğini tahmin edebilirsiniz.
- Aşağıdaki kodu kullanarak bir hesaplama gölgelendiricisi oluşturun:
index.html
// Create the compute shader that will process the simulation.
const simulationShaderModule = device.createShaderModule({
label: "Game of Life simulation shader",
code: `
@compute
fn computeMain() {
}`
});
GPU'lar 3D grafikler için sıkça kullanıldığından işlem gölgelendiricileri, gölgelendiricinin X, Y ve Z eksenleri boyunca belirli sayıda çağrılmasını isteyebileceğiniz şekilde yapılandırılır. Bu sayede, 2D veya 3D ızgaraya uygun işleri çok kolay bir şekilde gönderebilirsiniz. Bu da kullanım alanınız için harika bir özelliktir. Bu gölgelendiriciyi, simülasyonunuzun her hücresi için bir kez olmak üzere GRID_SIZE
kez çağırmak istiyorsunuz.GRID_SIZE
GPU donanım mimarisinin yapısı nedeniyle bu ızgara çalışma gruplarına ayrılır. Çalışma gruplarının X, Y ve Z boyutları vardır. Bu boyutlar 1 olabilir ancak çalışma gruplarınızı biraz daha büyük hale getirmenin genellikle performans açısından faydaları vardır. Gölgelendiriciniz için 8 x 8 gibi biraz rastgele bir iş grubu boyutu seçin. Bu, JavaScript kodunuzda takip etmek için kullanışlıdır.
- Çalışma grubu boyutunuz için şu şekilde bir sabit tanımlayın:
index.html
const WORKGROUP_SIZE = 8;
Ayrıca, az önce tanımladığınız sabiti kolayca kullanabilmek için JavaScript'in şablon değişmezlerini kullanarak çalışma grubu boyutunu gölgelendirici işlevine de eklemeniz gerekir.
- Çalışma grubu boyutunu gölgelendirici işlevine aşağıdaki gibi ekleyin:
index.html (Compute createShaderModule call)
@compute
@workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}) // New line
fn computeMain() {
}
Bu, gölgelendiriciye bu işlevle yapılan çalışmanın (8 x 8 x 1) gruplar halinde yapıldığını bildirir. (Belirtmediğiniz tüm eksenler varsayılan olarak 1 olur ancak en azından X eksenini belirtmeniz gerekir.)
Diğer gölgelendirici aşamalarında olduğu gibi, hangi çağırmada olduğunuzu anlamak ve hangi işi yapmanız gerektiğine karar vermek için hesaplama gölgelendirici işlevinize giriş olarak kabul edebileceğiniz çeşitli @builtin
değerleri vardır.
- Şu şekilde bir
@builtin
değeri ekleyin:
index.html (Compute createShaderModule call)
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
global_invocation_id
yerleşik işlevini iletirsiniz. Bu işlev, gölgelendirici çağırma işlemlerinin ızgarasında nerede olduğunuzu gösteren, işaretsiz tam sayılardan oluşan üç boyutlu bir vektördür. Bu gölgelendiriciyi tablonuzdaki her hücre için bir kez çalıştırırsınız. (0, 0, 0)
, (1, 0, 0)
, (1, 1, 0)
... (31, 31, 0)
'e kadar olan sayıları elde edersiniz. Bu da bu sayıları üzerinde işlem yapacağınız hücre dizini olarak değerlendirebileceğiniz anlamına gelir.
İşlem gölgelendiriciler, köşe ve parça gölgelendiricilerde olduğu gibi tek tip değişkenler de kullanabilir.
- Izgara boyutunu bildirmek için aşağıdaki gibi hesaplama gölgelendiricinizle birlikte bir tek tip kullanın:
index.html (Compute createShaderModule call)
@group(0) @binding(0) var<uniform> grid: vec2f; // New line
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
Köşe gölgelendiricisinde olduğu gibi, hücre durumunu da depolama arabelleği olarak kullanıma sunarsınız. Ancak bu durumda ikisine de ihtiyacınız var. Compute shader'ların köşe konumu veya parça rengi gibi zorunlu bir çıkışı olmadığından, sonuçları compute shader'dan almanın tek yolu değerleri bir depolama arabelleğine veya dokuya yazmaktır. Daha önce öğrendiğiniz ping-pong yöntemini kullanın. Izgaranın mevcut durumunu besleyen bir depolama arabelleğiniz ve ızgaranın yeni durumunu yazdığınız bir arabelleğiniz vardır.
- Hücre giriş ve çıkış durumunu şu şekilde depolama arabellekleri olarak kullanıma sunun:
index.html (Compute createShaderModule call)
@group(0) @binding(0) var<uniform> grid: vec2f;
// New lines
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
İlk depolama arabelleğinin var<storage>
ile tanımlandığını ve bu nedenle salt okunur olduğunu, ikinci depolama arabelleğinin ise var<storage, read_write>
ile tanımlandığını unutmayın. Bu sayede, arabelleği okuyup yazabilir ve arabelleği hesaplama gölgelendiricinizin çıkışı olarak kullanabilirsiniz. (WebGPU'da yalnızca yazma depolama modu yoktur.)
Ardından, hücre dizininizi doğrusal depolama dizisine eşlemenin bir yolunu bulmanız gerekir. Bu, temel olarak köşe gölgelendiricide yaptığınız işlemin tam tersidir. Köşe gölgelendiricide doğrusal instance_index
değerini alıp 2D ızgara hücresine eşlemiştiniz. (Bu işlem için algoritmanızın vec2f(i % grid.x, floor(i / grid.x))
olduğunu hatırlatırız.)
- Diğer yöne gidecek bir fonksiyon yazın. Hücrenin Y değerini alır, ızgara genişliğiyle çarpar ve ardından hücrenin X değerini ekler.
index.html (Compute createShaderModule call)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
// New function
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
}
Son olarak, algoritmanın çalıştığını görmek için çok basit bir algoritma uygulayın: Hücre şu anda açıksa kapanır, kapalıysa açılır. Bu örnek henüz Hayat Oyunu değil ancak hesaplama gölgelendiricisinin çalıştığını göstermek için yeterli.
- Basit algoritmayı aşağıdaki gibi ekleyin:
index.html (Compute createShaderModule call)
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return cell.y * u32(grid.x) + cell.x;
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// New lines. Flip the cell state every step.
if (cellStateIn[cellIndex(cell.xy)] == 1) {
cellStateOut[cellIndex(cell.xy)] = 0;
} else {
cellStateOut[cellIndex(cell.xy)] = 1;
}
}
Şimdilik hesaplama gölgelendiricinizle ilgili bilgiler bu kadar. Ancak sonuçları görebilmeniz için yapmanız gereken birkaç değişiklik daha var.
Bind Group ve Pipeline Layouts'u kullanma
Yukarıdaki gölgelendiriciden fark edebileceğiniz bir nokta, bunun büyük ölçüde oluşturma işlem hattınızla aynı girişleri (tek tip ve depolama arabellekleri) kullanmasıdır. Bu nedenle, aynı bağlama gruplarını kullanıp işi bitirebileceğinizi düşünebilirsiniz, değil mi? İyi haberimiz var: Bunu yapabilirsiniz. Bunu yapabilmek için biraz daha fazla manuel kurulum yapmanız gerekir.
Herhangi bir bağlama grubu oluşturduğunuzda GPUBindGroupLayout
sağlamanız gerekir. Daha önce, oluşturma işlem hattında getBindGroupLayout()
çağrısı yaparak bu düzeni elde ediyordunuz. Bu da oluşturma işlem hattını oluştururken layout: "auto"
sağladığınız için düzeni otomatik olarak oluşturuyordu. Bu yaklaşım yalnızca tek bir işlem hattı kullandığınızda iyi sonuç verir. Ancak kaynakları paylaşmak isteyen birden fazla işlem hattınız varsa düzeni açıkça oluşturmanız ve ardından hem bağlama grubuna hem de işlem hatlarına sağlamanız gerekir.
Bunun nedenini anlamak için şu örneği inceleyin: Oluşturma işlem hatlarınızda tek bir tekdüzen arabellek ve tek bir depolama arabelleği kullanıyorsunuz ancak yeni yazdığınız hesaplama gölgelendiricisinde ikinci bir depolama arabelleğine ihtiyacınız var. İki gölgelendirici, tek tip ve ilk depolama arabelleği için aynı @binding
değerlerini kullandığından bunları işlem hatları arasında paylaşabilirsiniz. Oluşturma işlem hattı ise kullanmadığı ikinci depolama arabelleğini yoksayar. Yalnızca belirli bir işlem hattı tarafından kullanılan kaynakları değil, bağlama grubunda bulunan tüm kaynakları açıklayan bir düzen oluşturmak istiyorsunuz.
- Bu düzeni oluşturmak için
device.createBindGroupLayout()
işlevini çağırın:
index.html
// Create the bind group layout and pipeline layout.
const bindGroupLayout = device.createBindGroupLayout({
label: "Cell Bind Group Layout",
entries: [{
binding: 0,
// Add GPUShaderStage.FRAGMENT here if you are using the `grid` uniform in the fragment shader.
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: {} // Grid uniform buffer
}, {
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: "read-only-storage"} // Cell state input buffer
}, {
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage"} // Cell state output buffer
}]
});
Bu, entries
listesini tanımladığınız için bağlama grubunu oluşturmaya benzer bir yapıya sahiptir. Aradaki fark, kaynağın kendisini sağlamak yerine girişin ne tür bir kaynak olması gerektiğini ve nasıl kullanıldığını açıklamanızdır.
Her girişte, kaynağın binding
numarasını verirsiniz. Bu numara (bağlama grubunu oluştururken öğrendiğiniz gibi) gölgelendiricilerdeki @binding
değeriyle eşleşir. Ayrıca, kaynağı hangi gölgelendirici aşamalarının kullanabileceğini belirten GPUShaderStage
işaretleri olan visibility
değerini de sağlarsınız. Hem tekdüzen hem de ilk depolama arabelleğinin tepe noktası ve hesaplama gölgelendiricilerinde erişilebilir olmasını istiyorsunuz ancak ikinci depolama arabelleğinin yalnızca hesaplama gölgelendiricilerinde erişilebilir olması gerekiyor.
Son olarak, kullanılan kaynak türünü belirtirsiniz. Bu, neyi göstermeniz gerektiğine bağlı olarak farklı bir sözlük anahtarıdır. Burada üç kaynağın tamamı arabellek olduğundan her birinin seçeneklerini tanımlamak için buffer
tuşunu kullanırsınız. texture
veya sampler
gibi başka seçenekler de vardır ancak burada bunlara ihtiyacınız yoktur.
Arabellek sözlüğünde, ne kadar arabellek kullanılacağı gibi seçenekleri ayarlarsınız.type
Varsayılan değer "uniform"
olduğundan sözlüğü 0 bağlama için boş bırakabilirsiniz. (Ancak girişin arabellek olarak tanımlanması için en az buffer: {}
değerini ayarlamanız gerekir.) Bağlama 1'e "read-only-storage"
türü verilir çünkü bunu gölgelendiricide read_write
erişimiyle kullanmazsınız. Bağlama 2'ye ise "storage"
türü verilir çünkü bunu read_write
erişimiyle kullanırsınız.
bindGroupLayout
oluşturulduktan sonra, bağlama gruplarınızı oluştururken bağlama grubunu işlem hattından sorgulamak yerine bindGroupLayout
değerini iletebilirsiniz. Bu durumda, az önce tanımladığınız düzene uymak için her bağlama grubuna yeni bir depolama arabelleği girişi eklemeniz gerekir.
- Bağlama grubu oluşturma işlemini aşağıdaki gibi güncelleyin:
index.html
// Create a bind group to pass the grid uniforms into the pipeline
const bindGroups = [
device.createBindGroup({
label: "Cell renderer bind group A",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[0] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[1] }
}],
}),
device.createBindGroup({
label: "Cell renderer bind group B",
layout: bindGroupLayout, // Updated Line
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}, {
binding: 1,
resource: { buffer: cellStateStorage[1] }
}, {
binding: 2, // New Entry
resource: { buffer: cellStateStorage[0] }
}],
}),
];
Bağlama grubu, bu açık bağlama grubu düzenini kullanacak şekilde güncellendiğinden oluşturma işlem hattını da aynı şeyi kullanacak şekilde güncellemeniz gerekir.
GPUPipelineLayout
oluşturun.
index.html
const pipelineLayout = device.createPipelineLayout({
label: "Cell Pipeline Layout",
bindGroupLayouts: [ bindGroupLayout ],
});
Bir işlem hattı düzeni, bir veya daha fazla işlem hattının kullandığı bağlama grubu düzenlerinin bir listesidir (bu durumda bir tane vardır). Dizideki bağlama grubu düzenlerinin sırası, gölgelendiricilerdeki @group
özellikleriyle eşleşmelidir. (Bu, bindGroupLayout
hesabının @group(0)
ile ilişkilendirildiği anlamına gelir.)
- İşlem hattı düzenini aldıktan sonra, oluşturma işlem hattını
"auto"
yerine bu düzeni kullanacak şekilde güncelleyin.
index.html
const cellPipeline = device.createRenderPipeline({
label: "Cell pipeline",
layout: pipelineLayout, // Updated!
vertex: {
module: cellShaderModule,
entryPoint: "vertexMain",
buffers: [vertexBufferLayout]
},
fragment: {
module: cellShaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat
}]
}
});
İşlem ardışık düzenini oluşturma
Köşe ve parça gölgelendiricilerinizi kullanmak için oluşturma işlem hattına ihtiyacınız olduğu gibi, hesaplama gölgelendiricinizi kullanmak için de hesaplama işlem hattına ihtiyacınız vardır. Neyse ki işlem ardışık düzenleri, oluşturma ardışık düzenlerine kıyasla çok daha az karmaşıktır. Çünkü ayarlanacak herhangi bir durumu yoktur, yalnızca gölgelendirici ve düzen vardır.
- Aşağıdaki kodu kullanarak bir işlem ardışık düzeni oluşturun:
index.html
// Create a compute pipeline that updates the game state.
const simulationPipeline = device.createComputePipeline({
label: "Simulation pipeline",
layout: pipelineLayout,
compute: {
module: simulationShaderModule,
entryPoint: "computeMain",
}
});
Güncellenen oluşturma işlem hattında olduğu gibi, "auto"
yerine yeni pipelineLayout
öğesini ilettiğinizi unutmayın. Bu, hem oluşturma işlem hattınızın hem de hesaplama işlem hattınızın aynı bağlama gruplarını kullanabilmesini sağlar.
Compute geçişleri
Bu noktada işlem hattını gerçekten kullanmaya başlayabilirsiniz. Render işleminizi bir render geçişinde yaptığınızı göz önünde bulundurarak, hesaplama işlemini bir hesaplama geçişinde yapmanız gerektiğini tahmin edebilirsiniz. Hem hesaplama hem de oluşturma işlemleri aynı komut kodlayıcıda gerçekleşebilir. Bu nedenle, updateGrid
işlevinizi biraz karıştırmanız gerekir.
- Kodlayıcı oluşturma işlemini işlevin en üstüne taşıyın ve ardından
step++
'dan önce kodlayıcıyla bir hesaplama geçişi başlatın.
index.html
// In updateGrid()
// Move the encoder creation to the top of the function.
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
// Compute work will go here...
computePass.end();
// Existing lines
step++; // Increment the step count
// Start a render pass...
İşlem geçişleri, işlem işlem hatları gibi eklerle ilgili endişelenmenize gerek olmadığından oluşturma karşılıklarına kıyasla çok daha kolay başlatılır.
Hesaplama geçişinin sonuçları, oluşturma geçişinde hemen kullanılabildiğinden hesaplama geçişini oluşturma geçişinden önce yapmak istersiniz. Bu nedenle, geçişler arasında step
sayısını artırarak hesaplama işlem hattının çıkış arabelleğini oluşturma işlem hattının giriş arabelleği haline getirirsiniz.
- Ardından, işlem geçişi içinde işlem hattını ayarlayın ve bağlama grubunu bağlayın. Bağlama grupları arasında geçiş yapmak için oluşturma geçişinde kullandığınız kalıbı kullanın.
index.html
const computePass = encoder.beginComputePass();
// New lines
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
computePass.end();
- Son olarak, oluşturma geçişinde olduğu gibi çizim yapmak yerine, çalışmayı hesaplama gölgelendiricisine göndererek her eksende kaç çalışma grubu yürütmek istediğinizi belirtirsiniz.
index.html
const computePass = encoder.beginComputePass();
computePass.setPipeline(simulationPipeline);
computePass.setBindGroup(0, bindGroups[step % 2]);
// New lines
const workgroupCount = Math.ceil(GRID_SIZE / WORKGROUP_SIZE);
computePass.dispatchWorkgroups(workgroupCount, workgroupCount);
computePass.end();
Burada çok önemli bir nokta, dispatchWorkgroups()
öğesine ilettiğiniz sayının, çağırma sayısı olmamasıdır. Bunun yerine, gölgelendiricinizdeki @workgroup_size
tarafından tanımlanan, yürütülecek iş grubu sayısıdır.
Shader'ın tüm ızgaranızı kaplamak için 32x32 kez yürütülmesini istiyorsanız ve iş grubu boyutunuz 8x8 ise 4x4 iş grubu göndermeniz gerekir (4 * 8 = 32). Bu nedenle, ızgara boyutunu çalışma grubu boyutuna bölüp bu değeri dispatchWorkgroups()
'ya aktarırsınız.
Şimdi sayfayı tekrar yenileyebilirsiniz. Izgaranın her güncellemede tersine döndüğünü görürsünüz.
Hayat Oyunu algoritmasını uygulama
Nihai algoritmayı uygulamak için bilgi işlem gölgelendiricisini güncellemeden önce, depolama arabelleği içeriğini başlatan koda geri dönüp her sayfa yüklemesinde rastgele bir arabellek oluşturacak şekilde güncellemek istersiniz. (Düzenli desenler, Yaşam Oyunu için çok ilginç başlangıç noktaları oluşturmaz.) Değerleri istediğiniz gibi rastgele hale getirebilirsiniz ancak makul sonuçlar veren kolay bir başlangıç yöntemi vardır.
- Her hücreye rastgele bir durumda başlamak için
cellStateArray
başlatma işlemini aşağıdaki kodla güncelleyin:
index.html
// Set each cell to a random state, then copy the JavaScript array
// into the storage buffer.
for (let i = 0; i < cellStateArray.length; ++i) {
cellStateArray[i] = Math.random() > 0.6 ? 1 : 0;
}
device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray);
Artık nihayet Game of Life simülasyonunun mantığını uygulayabilirsiniz. Buraya gelmek için gereken her şeyden sonra gölgelendirici kodu hayal kırıklığı yaratacak kadar basit olabilir.
Öncelikle, herhangi bir hücrenin kaç komşusunun etkin olduğunu bilmeniz gerekir. Hangilerinin etkin olduğuyla değil, yalnızca sayısıyla ilgileniyorsunuz.
- Komşu hücre verilerini almayı kolaylaştırmak için, belirli koordinatın
cellStateIn
değerini döndüren bircellActive
işlevi ekleyin.
index.html (Compute createShaderModule call)
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
cellActive
işlevi, hücre etkinse 1 değerini döndürür. Bu nedenle, çevreleyen sekiz hücre için cellActive
işlevini çağırmanın dönüş değerini eklediğinizde kaç komşu hücrenin etkin olduğunu görürsünüz.
- Etkin komşu sayısını şu şekilde bulabilirsiniz:
index.html (Compute createShaderModule call)
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// New lines:
// Determine how many active neighbors this cell has.
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);
Ancak bu durum küçük bir soruna yol açar: Kontrol ettiğiniz hücre tahtanın kenarında değilse ne olur? Şu anki cellIndex()
mantığınıza göre, ya sonraki veya önceki satıra taşar ya da arabelleğin kenarından çıkar.
Hayat Oyunu'nda bu sorunu çözmenin yaygın ve kolay bir yolu, ızgaranın kenarındaki hücrelerin, ızgaranın karşı kenarındaki hücreleri komşuları olarak kabul etmesini sağlamaktır. Bu şekilde, bir tür sarma efekti oluşturulur.
cellIndex()
işlevinde küçük bir değişiklik yaparak ızgara sarmalama desteği eklendi.
index.html (Compute createShaderModule call)
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
Izgara boyutunu aşan X ve Y hücrelerini sarmak için %
operatörünü kullanarak depolama arabelleği sınırlarının dışına hiçbir zaman erişmemeyi sağlayabilirsiniz. Bu sayede activeNeighbors
sayısının tahmin edilebilir olduğundan emin olabilirsiniz.
Ardından, dört kuraldan birini uygularsınız:
- İkiden az komşusu olan hücreler etkinliğini kaybeder.
- İki veya üç komşusu olan etkin hücreler etkin kalır.
- Tam olarak üç komşusu olan tüm etkin olmayan hücreler etkin hale gelir.
- Üçten fazla komşusu olan hücreler etkinliğini kaybeder.
Bunu bir dizi if ifadesiyle yapabilirsiniz ancak WGSL, bu mantık için uygun olan switch ifadelerini de destekler.
- Game of Life mantığını aşağıdaki gibi uygulayın:
index.html (Compute createShaderModule call)
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: { // Active cells with 2 neighbors stay active.
cellStateOut[i] = cellStateIn[i];
}
case 3: { // Cells with 3 neighbors become or stay active.
cellStateOut[i] = 1;
}
default: { // Cells with < 2 or > 3 neighbors become inactive.
cellStateOut[i] = 0;
}
}
Referans olarak, nihai hesaplama gölgelendirici modülü çağrısı artık şu şekilde görünür:
index.html
const simulationShaderModule = device.createShaderModule({
label: "Life simulation shader",
code: `
@group(0) @binding(0) var<uniform> grid: vec2f;
@group(0) @binding(1) var<storage> cellStateIn: array<u32>;
@group(0) @binding(2) var<storage, read_write> cellStateOut: array<u32>;
fn cellIndex(cell: vec2u) -> u32 {
return (cell.y % u32(grid.y)) * u32(grid.x) +
(cell.x % u32(grid.x));
}
fn cellActive(x: u32, y: u32) -> u32 {
return cellStateIn[cellIndex(vec2(x, y))];
}
@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE})
fn computeMain(@builtin(global_invocation_id) cell: vec3u) {
// Determine how many active neighbors this cell has.
let activeNeighbors = cellActive(cell.x+1, cell.y+1) +
cellActive(cell.x+1, cell.y) +
cellActive(cell.x+1, cell.y-1) +
cellActive(cell.x, cell.y-1) +
cellActive(cell.x-1, cell.y-1) +
cellActive(cell.x-1, cell.y) +
cellActive(cell.x-1, cell.y+1) +
cellActive(cell.x, cell.y+1);
let i = cellIndex(cell.xy);
// Conway's game of life rules:
switch activeNeighbors {
case 2: {
cellStateOut[i] = cellStateIn[i];
}
case 3: {
cellStateOut[i] = 1;
}
default: {
cellStateOut[i] = 0;
}
}
}
`
});
İşlem tamamlandı. Hepsi bu kadar! Sayfanızı yenileyin ve yeni oluşturduğunuz hücresel otomatın büyümesini izleyin.
9. Tebrikler!
WebGPU API'yi kullanarak klasik Conway'in Yaşam Oyunu simülasyonunun tamamen GPU'nuzda çalışan bir sürümünü oluşturdunuz.
Yapabilecekleriniz
- WebGPU örneklerini inceleyin.