Emscripten ile WebAssembly'den web'de 2D grafikleri nasıl oluşturacağınızı öğrenin.
Farklı işletim sistemlerinde grafik çizmek için farklı API'ler kullanılır. Platformlar arası kod yazarken veya yerel kodu WebAssembly'ye taşıma işlemi de dahil olmak üzere grafikleri bir sistemden diğerine taşırken farklılıklar daha da karmaşık hale gelir.
Bu yayında, Emscripten ile derlenmiş C veya C++ kodundan web'deki tuval öğesine 2D grafik çizmek için birkaç yöntem öğreneceksiniz.
Embind aracılığıyla Canvas
Mevcut bir projeyi taşımaya çalışmak yerine yeni bir proje başlatıyorsanız en kolay yöntem, Emscripten'in Embind bağlama sistemi üzerinden HTML Canvas API'yi kullanmak olabilir. Embind, rastgele JavaScript değerleri üzerinde doğrudan çalışmanıza olanak tanır.
Embind'in nasıl kullanılacağını anlamak için önce bir <canvas> öğesi bulup üzerine bazı şekiller çizen MDN'deki aşağıdaki örneğe göz atın.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);
Embind ile C++ diline nasıl dönüştürülebileceği aşağıda açıklanmıştır:
#include <emscripten/val.h>
using emscripten::val;
// Use thread_local when you want to retrieve & cache a global JS variable once per thread.
thread_local const val document = val::global("document");
// …
int main() {
val canvas = document.call<val>("getElementById", "canvas");
val ctx = canvas.call<val>("getContext", "2d");
ctx.set("fillStyle", "green");
ctx.call<void>("fillRect", 10, 10, 150, 100);
}
Bu kodu bağlarken, Embind'i etkinleştirmek için --bind
parametresini ilettiğinizden emin olun:
emcc --bind example.cpp -o example.html
Ardından, derlenmiş öğeleri statik bir sunucuda yayınlayabilir ve örneği bir tarayıcıya yükleyebilirsiniz:
Tuval öğesini seçme
Emscripten tarafından oluşturulan HTML kabuğunu önceki kabuk komutuyla kullandığınızda tuval dahil edilir ve sizin için ayarlanır. Basit demolar ve örnekler oluşturmayı kolaylaştırır, ancak daha büyük uygulamalarda Emscripten tarafından oluşturulan JavaScript ve WebAssembly'yi kendi tasarımınızın bir HTML sayfasına eklemek istersiniz.
Oluşturulan JavaScript kodu, Module.canvas
mülkünde depolanan kanvas öğesini bulmayı bekler. Diğer modül özellikleri gibi, bu özellik de ilk başlatma sırasında ayarlanabilir.
ES6 modunu kullanıyorsanız (çıktıyı .mjs
uzantılı bir yola ayarlayarak veya -s EXPORT_ES6
ayarını kullanarak) kanvası şu şekilde iletebilirsiniz:
import initModule from './emscripten-generated.mjs';
const Module = await initModule({
canvas: document.getElementById('my-canvas')
});
Normal komut dosyası çıkışı kullanıyorsanız Emscripten tarafından oluşturulan JavaScript dosyasını yüklemeden önce Module
nesnesini tanımlamanız gerekir:
<script>
var Module = {
canvas: document.getElementById('my-canvas')
};
</script>
<script src="emscripten-generated.js"></script>
OpenGL ve SDL2
OpenGL, bilgisayar grafikleri için popüler bir platformlar arası API'dir. Emscripten'de kullanıldığında, desteklenen OpenGL işlemlerinin alt kümesini WebGL'ye dönüştürür. Uygulamanız OpenGL ES 2.0 veya 3.0'da desteklenen özelliklere dayanıyor ancak WebGL'de desteklenmiyorsa Emscripten, bunların emülasyonunu da yapabilir, ancak ilgili ayarları kullanarak etkinleştirmeniz gerekir.
OpenGL'i doğrudan veya daha üst düzey 2D ve 3D grafik kitaplıkları aracılığıyla kullanabilirsiniz. Bunlardan birkaçı Emscripten ile web'e taşındı. Bu yayında 2D grafiklere odaklanıyorum. Bu nedenle, iyi test edilmiş ve Emscripten arka ucunu resmi olarak desteklediği için şu anda tercih edilen kitaplık SDL2.
Dikdörtgen çizme
Resmi web sitesinin "SDL hakkında" bölümünde şunlar belirtilmektedir:
Basit DirectMedia Katmanı, OpenGL ve Direct3D aracılığıyla ses, klavye, fare, kontrol çubuğu ve grafik donanımına alt düzey erişim sağlamak için tasarlanmış platformlar arası bir geliştirme kitaplığıdır.
Ses, klavye, fare ve grafikleri kontrol etme gibi tüm bu özellikler, web'de Emscripten ile birlikte çalışacak şekilde taşındı. Böylece, SDL2 ile oluşturulan oyunların tamamını çok fazla sorun yaşamadan taşıyabilirsiniz. Mevcut bir projeyi taşıyorsanız Emscripten dokümanlarının "Bir derleme sistemiyle entegrasyon" bölümüne göz atın.
Basitlik sağlaması için bu gönderide tek dosyalık bir işleme odaklanacağım ve önceki dikdörtgen örneğini SDL2'ye çevireceğim:
#include <SDL2/SDL.h>
int main() {
// Initialize SDL graphics subsystem.
SDL_Init(SDL_INIT_VIDEO);
// Initialize a 300x300 window and a renderer.
SDL_Window *window;
SDL_Renderer *renderer;
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
// Set a color for drawing matching the earlier `ctx.fillStyle = "green"`.
SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
// Create and draw a rectangle like in the earlier `ctx.fillRect()`.
SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
SDL_RenderFillRect(renderer, &rect);
// Render everything from a buffer to the actual screen.
SDL_RenderPresent(renderer);
// TODO: cleanup
}
Emscripten ile bağlantı oluştururken -s USE_SDL=2
kullanmanız gerekir. Bu, Emscripten'e önceden WebAssembly olarak derlenmiş SDL2 kitaplığını getirmesini ve ana uygulamanıza bağlamasını söyler.
emcc example.cpp -o example.html -s USE_SDL=2
Örnek tarayıcıya yüklendiğinde, bilinen yeşil dikdörtgeni görürsünüz:
Ancak bu kodda birkaç sorun var. Öncelikle, ayrılan kaynakların düzgün şekilde temizlenmemesi. İkinci olarak, web'de bir uygulamanın çalışması tamamlandığında sayfalar otomatik olarak kapanmaz. Bu nedenle, tuvaldeki resim korunur. Ancak aynı kod, doğal olarak yeniden derlendiğinde
clang example.cpp -o example -lSDL2
ve çalıştırılırsa oluşturulan pencere yalnızca kısa bir süre yanıp söner ve kullanıcının resmi görme şansı olmadığı için hemen kapanır.
Etkinlik döngüsünü entegre etme
Daha kapsamlı ve doğal bir örnek, kullanıcı uygulamadan çıkmayı seçene kadar bir etkinlik döngüsünde beklemeyi gerektirir:
#include <SDL2/SDL.h>
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *window;
SDL_Renderer *renderer;
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
SDL_RenderFillRect(renderer, &rect);
SDL_RenderPresent(renderer);
while (1) {
SDL_Event event;
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
break;
}
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
Resim bir pencereye çizildikten sonra uygulama, klavye, fare ve diğer kullanıcı etkinliklerini işleyebileceği bir döngüde bekler. Kullanıcı pencereyi kapattığında döngüden çıkmak için müdahale edilecek bir SDL_QUIT
etkinliği tetiklenir. Döngüden çıkıldıktan sonra uygulama temizleme işlemini gerçekleştirir ve kendisinden çıkar.
Bu örnek Linux'ta derlendiğinde beklendiği gibi çalışır ve yeşil bir dikdörtgen ile 300x300 boyutunda bir pencere gösterir:
Ancak bu örnek artık web'de çalışmamaktadır. Emscripten tarafından oluşturulan sayfa, yükleme sırasında hemen donar ve oluşturulan resmi hiçbir zaman göstermez:
Ne oldu? "WebAssembly'den eşzamansız web API'lerini kullanma" makalesindeki yanıtı alıntılayacağım:
Kısaca, tarayıcı tüm kod parçalarını kuyruktan tek tek alarak bir tür sonsuz döngüde çalıştırır. Bir etkinlik tetiklendiğinde, tarayıcı ilgili işleyiciyi sıraya alır ve bir sonraki döngü yinelemesinde sıradan çıkarılarak yürütülür. Bu mekanizma, eşzamanlılığın simüle edilmesine ve yalnızca tek bir iş parçacığı kullanılarak çok sayıda paralel işlem yürütülmesine olanak tanır.
Bu mekanizmayla ilgili akılda tutulması gereken önemli nokta, özel JavaScript (veya WebAssembly) kodunuz yürütülürken etkinlik döngüsünün engellenmesidir. […]
Yukarıdaki örnek, bir sonsuz etkinlik döngüsü yürütürken kodun kendisi de tarayıcı tarafından dolaylı olarak sağlanan başka bir sonsuz etkinlik döngüsü içinde çalışır. İç döngü, kontrolü hiçbir zaman dış döngüye bırakmaz. Bu nedenle tarayıcı, harici etkinlikleri işleme veya sayfaya bir şeyler çizme şansı bulamaz.
Bu sorunu çözmenin iki yolu vardır.
Asyncify ile etkinlik döngüsünün engellemesini kaldırma
İlk olarak, bağlantılı makalede açıklandığı gibi Asyncify'ı kullanabilirsiniz. C veya C++ programını "duraklatmaya", etkinlik döngüsüne kontrol sağlamaya ve bazı eşzamansız işlemler tamamlandığında programı uyandırmaya olanak tanıyan bir Emscripten özelliğidir.
Bu tür eşzamansız işlemler, emscripten_sleep(0)
API aracılığıyla ifade edilen "mümkün olan en kısa süre boyunca bekleme" şeklinde bile olabilir. Bu işlevi döngünün ortasına yerleştirerek, her iterasyonda kontrolün tarayıcı etkinliği döngüsüne döndürülmesini ve sayfanın duyarlı kalmasını ve tüm etkinlikleri işleyebilmesini sağlayabilirim:
#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *window;
SDL_Renderer *renderer;
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
SDL_RenderFillRect(renderer, &rect);
SDL_RenderPresent(renderer);
while (1) {
SDL_Event event;
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
break;
}
#ifdef __EMSCRIPTEN__
emscripten_sleep(0);
#endif
}
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
Bu kodun Asyncify etkinken derlenmesi gerekir:
emcc example.cpp -o example.html -s USE_SDL=2 -s ASYNCIFY
Uygulama, web'de tekrar beklendiği gibi çalışıyor:
Ancak Asyncify, önemsiz kod boyutu ek yüküne neden olabilir. Yalnızca uygulamadaki üst düzey bir etkinlik döngüsü için kullanılıyorsa daha iyi bir seçenek emscripten_set_main_loop
işlevini kullanmak olabilir.
"Ana döngü" API'leriyle etkinlik döngüsünün engellemesini kaldırma
emscripten_set_main_loop
, çağrı yığınını sarmalamak ve geri sarmak için herhangi bir derleyici dönüşümü gerektirmez. Bu sayede kod boyutu yükü önlenir. Ancak bunun karşılığında kodda çok daha fazla manuel değişiklik yapmak gerekir.
Öncelikle, etkinlik döngüsünün gövdesinin ayrı bir işleve çıkarılması gerekir. Ardından, ilk bağımsız değişkende geri çağırma işleviyle emscripten_set_main_loop
, ikinci bağımsız değişkende FPS (yerel yenileme aralığı için 0
) ve üçüncü bağımsız değişkende sonsuz döngü (true
) simülasyonunun yapılıp yapılmayacağını gösteren bir boole olarak bu işlevle çağrılmalıdır:
emscripten_set_main_loop(callback, 0, true);
Yeni oluşturulan geri çağırmanın main
işlevindeki yığın değişkenlerine erişimi olmayacağından, window
ve renderer
gibi değişkenlerin yığınla ayrılmış bir struct'a çıkarılması ve işaretçisinin API'nin emscripten_set_main_loop_arg
varyantı üzerinden geçirilmesi ya da genel static
değişkenlerine (basitlik için ikinciyi tercih ettim) çıkarılması gerekir. Sonucu takip etmek biraz daha zordur ancak son örnektekiyle aynı dikdörtgeni çizer:
#include <SDL2/SDL.h>
#include <stdio.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
SDL_Window *window;
SDL_Renderer *renderer;
bool handle_events() {
SDL_Event event;
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
return false;
}
return true;
}
void run_main_loop() {
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
while (handle_events())
;
#endif
}
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
SDL_RenderFillRect(renderer, &rect);
SDL_RenderPresent(renderer);
run_main_loop();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
Tüm kontrol akışı değişiklikleri manuel olarak yapılır ve kaynak koda yansıtılır. Bu nedenle, kod tekrar Asyncify özelliği olmadan derlenebilir:
emcc example.cpp -o example.html -s USE_SDL=2
Bu örnek, kodun çok daha basit olmasına rağmen dikdörtgenin tuvale başarıyla çizildiği ilk sürümden farklı olmadığı ve handle_events
işlevinde işlenen tek etkinlik olan SDL_QUIT
etkinliği web'de zaten yoksayıldığı için, bu örnek işe yaramaz görünebilir.
Ancak, herhangi bir animasyon veya etkileşim eklemeye karar verirseniz Asyncify veya emscripten_set_main_loop
aracılığıyla uygun etkinlik döngüsü entegrasyonunu kullanmak faydalı olacaktır.
Kullanıcı etkileşimlerini yönetme
Örneğin, son örnekte birkaç değişiklik yaparak dikdörtgeni klavye etkinliklerine göre hareket ettirebilirsiniz:
#include <SDL2/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Rect rect = {.x = 10, .y = 10, .w = 150, .h = 100};
void redraw() {
SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer);
SDL_SetRenderDrawColor(renderer, /* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
SDL_RenderFillRect(renderer, &rect);
SDL_RenderPresent(renderer);
}
uint32_t ticksForNextKeyDown = 0;
bool handle_events() {
SDL_Event event;
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
return false;
}
if (event.type == SDL_KEYDOWN) {
uint32_t ticksNow = SDL_GetTicks();
if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
// Throttle keydown events for 10ms.
ticksForNextKeyDown = ticksNow + 10;
switch (event.key.keysym.sym) {
case SDLK_UP:
rect.y -= 1;
break;
case SDLK_DOWN:
rect.y += 1;
break;
case SDLK_RIGHT:
rect.x += 1;
break;
case SDLK_LEFT:
rect.x -= 1;
break;
}
redraw();
}
}
return true;
}
void run_main_loop() {
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
while (handle_events())
;
#endif
}
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
redraw();
run_main_loop();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
SDL2_gfx ile diğer şekilleri çizme
SDL2, platformlar arası farklılıkları ve çeşitli medya cihazı türlerini tek bir API'de soyutlar ancak yine de oldukça düşük düzey bir kitaplıktır. Özellikle grafikler için çizim noktaları, çizgiler ve dikdörtgenler için API'ler sağlar, ancak daha karmaşık şekillerin ve dönüşümlerin uygulanması kullanıcıya bırakılır.
SDL2_gfx bu boşluğu dolduran ayrı bir kitaplıktır. Örneğin, yukarıdaki örnekte bir dikdörtgeni daireyle değiştirmek için kullanılabilir:
#include <SDL2/SDL.h>
#include <SDL2/SDL2_gfxPrimitives.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Point center = {.x = 100, .y = 100};
const int radius = 100;
void redraw() {
SDL_SetRenderDrawColor(renderer, /* RGBA: black */ 0x00, 0x00, 0x00, 0xFF);
SDL_RenderClear(renderer);
filledCircleRGBA(renderer, center.x, center.y, radius,
/* RGBA: green */ 0x00, 0x80, 0x00, 0xFF);
SDL_RenderPresent(renderer);
}
uint32_t ticksForNextKeyDown = 0;
bool handle_events() {
SDL_Event event;
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) {
return false;
}
if (event.type == SDL_KEYDOWN) {
uint32_t ticksNow = SDL_GetTicks();
if (SDL_TICKS_PASSED(ticksNow, ticksForNextKeyDown)) {
// Throttle keydown events for 10ms.
ticksForNextKeyDown = ticksNow + 10;
switch (event.key.keysym.sym) {
case SDLK_UP:
center.y -= 1;
break;
case SDLK_DOWN:
center.y += 1;
break;
case SDLK_RIGHT:
center.x += 1;
break;
case SDLK_LEFT:
center.x -= 1;
break;
}
redraw();
}
}
return true;
}
void run_main_loop() {
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop([]() { handle_events(); }, 0, true);
#else
while (handle_events())
;
#endif
}
int main() {
SDL_Init(SDL_INIT_VIDEO);
SDL_CreateWindowAndRenderer(300, 300, 0, &window, &renderer);
redraw();
run_main_loop();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
Artık SDL2_gfx kitaplığının da uygulamaya bağlanması gerekiyor. Bu işlem, SDL2'ye benzer şekilde yapılır:
# Native version
$ clang example.cpp -o example -lSDL2 -lSDL2_gfx
# Web version
$ emcc --bind foo.cpp -o foo.html -s USE_SDL=2 -s USE_SDL_GFX=2
Linux'ta çalışan sonuçlar şu şekildedir:
Web'de:
Daha fazla grafik ilkel öğesi için otomatik olarak oluşturulmuş dokümanlara göz atın.