Farklı işletim sistemlerinin grafik çizmek için farklı API’leri vardır. Platformlar arası bir kod yazarken veya grafikleri bir sistemden diğerine taşırken, yerel kodu WebAssembly’a taşırken bile farklılıklar daha da kafa karıştırıcı hale gelir.
Bu gönderide, Emscripten ile derlenmiş C veya C++ kodundan web üzerindeki canvas öğesine 2B grafikler çizmek için birkaç yöntem öğreneceksiniz.
Embind aracılığıyla tuval #
Mevcut bir projeyi taşımaya çalışmak yerine yeni bir projeye başlıyorsanız, HTML’yi kullanmak en kolayı olabilir. Tuval API’sı Emscripten’in ciltleme sistemi aracılığıyla Ortada. Gömme, doğrudan isteğe bağlı JavaScript değerleri üzerinde işlem yapmanızı sağlar.
Embind’in nasıl kullanılacağını anlamak için önce aşağıdakilere bir göz atın. MDN’den örnek bir <canvas> elemanı bulur ve üzerine bazı şekiller çizer.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);
Embind ile C++’a şu şekilde dönüştürülebilir:
#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.callval>("getElementById", "canvas");
val ctx = canvas.callval>("getContext", "2d");
ctx.set("fillStyle", "green");
ctx.callvoid>("fillRect", 10, 10, 150, 100);
}
Bu kodu bağlarken, geçtiğinizden emin olun. --bind
Gömmeyi etkinleştirmek için:
emcc --bind example.cpp -o example.html
Ardından, derlenmiş varlıkları statik bir sunucuyla sunabilir ve örneği bir tarayıcıya yükleyebilirsiniz:
Tuval öğesini seçme #
Önceki kabuk komutuyla Emscripten tarafından oluşturulan HTML kabuğunu kullanırken, 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’i ve WebAssembly’yi kendi tasarımınız olan bir HTML sayfasına dahil etmek isteyebilirsiniz.
Oluşturulan JavaScript kodu, içinde depolanan canvas öğesini bulmayı bekler. Module.canvas
mülk. Beğenmek diğer Modül özellikleribaşlatma sırasında ayarlanabilir.
ES6 modunu kullanıyorsanız (çıktıyı uzantılı bir yola ayarlama .mjs
veya kullanarak -s EXPORT_ES6
ayarı), tuvali şu şekilde iletebilirsiniz:
import initModule from './emscripten-generated.mjs';const Module = await initModule({
canvas: document.getElementById('my-canvas')
});
Normal betik çıktısı kullanıyorsanız, Module
Emscripten tarafından oluşturulan JavaScript dosyasını yüklemeden önce nesne:
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 çapraz platform API’sidir. Emscripten’de kullanıldığında, OpenGL işlemlerinin desteklenen alt kümesini dönüştürmekle ilgilenecektir. WebGL. Uygulamanız OpenGL ES 2.0 veya 3.0’da desteklenen ancak WebGL’de desteklenmeyen özelliklere dayanıyorsa, Emscripten bunları da taklit edebilir, ancak karşılık gelen ayarlar.
OpenGL’yi doğrudan veya daha üst düzey 2B ve 3B grafik kitaplıkları aracılığıyla kullanabilirsiniz. Bunlardan birkaçı Emscripten ile web’e taşındı. Bu yazıda, 2D grafiklere odaklanıyorum ve bunun için SDL2 şu anda tercih edilen kitaplıktır çünkü iyi bir şekilde test edilmiştir ve resmi olarak yukarı yönde Emscripten arka ucunu destekler.
dikdörtgen çizmek #
“SDL Hakkında” bölümü resmi internet sitesi diyor:
Simple DirectMedia Layer, OpenGL ve Direct3D aracılığıyla ses, klavye, fare, oyun çubuğu ve grafik donanımına düşük düzeyde erişim sağlamak için tasarlanmış bir çapraz platform geliştirme kitaplığıdır.
Sesi, klavyeyi, fareyi ve grafikleri kontrol eden tüm bu özellikler taşınmıştır ve web üzerinde Emscripten ile birlikte çalışır, böylece SDL2 ile oluşturulan tüm oyunları çok fazla güçlük çekmeden taşıyabilirsiniz. Mevcut bir projeyi taşıyorsanız şuna bakın: “Bir derleme sistemiyle bütünleştirme” Emscripten belgeleri bölümü.
Basit olması için, bu gönderide tek dosyalık bir vakaya 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ı kurarken kullanmanız gerekir -s USE_SDL=2
. Bu, Emscripten’e WebAssembly için önceden derlenmiş olan SDL2 kitaplığını getirmesini ve onu ana uygulamanızla bağlamasını söyleyecektir.
emcc example.cpp -o example.html -s USE_SDL=2
Örnek tarayıcıya yüklendiğinde, tanıdık yeşil dikdörtgeni göreceksiniz:
Bu kodun birkaç sorunu var. İlk olarak, tahsis edilen kaynakların uygun şekilde temizlenmesinden yoksundur. İkincisi, web’de, bir uygulama yürütmeyi bitirdiğinde sayfalar otomatik olarak kapanmaz, bu nedenle tuval üzerindeki görüntü korunur. Ancak, aynı kod yerel olarak yeniden derlendiğinde
clang example.cpp -o example -lSDL2
ve yürütüldüğünde, oluşturulan pencere yalnızca kısa bir süre yanıp söner ve çıkışta hemen kapanır, böylece kullanıcının görüntüyü görme şansı olmaz.
Bir olay döngüsünü entegre etme #
Daha eksiksiz ve deyimsel bir örnek, kullanıcı uygulamadan çıkmayı seçene kadar bir olay döngüsünde bekleme ihtiyacı olarak görünecektir:
#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();
}
Görüntü bir pencereye çizildikten sonra, uygulama artık klavye, fare ve diğer kullanıcı olaylarını işleyebileceği bir döngüde bekler. Kullanıcı pencereyi kapattığında, bir SDL_QUIT
döngüden çıkmak için yakalanacak olay. Döngüden çıkıldıktan sonra uygulama temizlemeyi yapacak ve ardından kendi kendine çıkacaktır.
Şimdi bu örneği Linux’ta derlemek beklendiği gibi çalışıyor ve yeşil bir dikdörtgenle 300’e 300’lük bir pencere gösteriyor:
Ancak, örnek artık web üzerinde çalışmıyor. Emscripten tarafından oluşturulan sayfa, yükleme sırasında hemen donuyor ve işlenmiş görüntüyü asla göstermiyor:
Ne oldu? “WebAssembly’den eşzamansız web API’lerini kullanma” makalesindeki yanıtı alıntılayacağım:
Kısa versiyon, tarayıcının tüm kod parçalarını sıradan birer birer alarak sonsuz bir döngü içinde çalıştırmasıdır. Bir olay tetiklendiğinde, tarayıcı ilgili işleyiciyi kuyruğa alır ve bir sonraki döngü yinelemesinde sıradan çıkarılır ve yürütülür. Bu mekanizma, yalnızca tek bir iş parçacığı kullanırken eşzamanlılığın simüle edilmesine ve çok sayıda paralel işlemin çalıştırılmasına izin verir.
Bu mekanizma hakkında hatırlanması gereken önemli nokta, özel JavaScript (veya WebAssembly) kodunuz yürütülürken olay döngüsünün engellenmiş olmasıdır. […]
Önceki örnek, sonsuz bir olay döngüsü yürütürken, kodun kendisi tarayıcı tarafından dolaylı olarak sağlanan başka bir sonsuz olay döngüsü içinde çalışır. İç döngü kontrolü asla dış döngüye bırakmaz, bu nedenle tarayıcının dış olayları işleme veya sayfaya bir şeyler çizme şansı olmaz.
Bu sorunu çözmenin iki yolu vardır.
Asyncify ile olay döngüsünün engelini kaldırma #
İlk olarak, bağlantılı makalede açıklandığı gibi kullanabilirsiniz Eşzamansızlaştır. Bu, C veya C++ programını “duraklatmaya”, kontrolü olay döngüsüne geri vermeye ve bazı eşzamansız işlemler bittiğinde programı uyandırmaya izin veren bir Emscripten özelliğidir.
Bu tür eşzamansız işlem, “mümkün olan minimum süre için uyku” bile olabilir, şu şekilde ifade edilir: emscripten_sleep(0)
API. Bunu döngünün ortasına yerleştirerek, kontrolün her yinelemede tarayıcının olay döngüsüne döndürülmesini ve sayfanın yanıt vermeye devam etmesini ve tüm olayları 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 artık Asyncify etkinken derlenmesi gerekiyor:
emcc example.cpp -o example.html -s USE_SDL=2 -s ASYNCIFY
Ve uygulama web’de tekrar beklendiği gibi çalışıyor:
Ancak Asyncify, önemsiz olmayan kod boyutu ek yüküne sahip olabilir. Uygulamada yalnızca üst düzey bir olay döngüsü için kullanılıyorsa, daha iyi bir seçenek, emscripten_set_main_loop
işlev.
“Ana döngü” API’leri ile olay döngüsünün engellemesini kaldırma #
emscripten_set_main_loop
çağrı yığınını çözmek ve geri sarmak için herhangi bir derleyici dönüşümü gerektirmez ve bu şekilde kod boyutu yükünden kaçınılır. Bununla birlikte, karşılığında, kodda çok daha fazla manuel değişiklik yapılmasını gerektirir.
İlk olarak, olay döngüsünün gövdesinin ayrı bir işleve çıkarılması gerekir. Daha sonra, emscripten_set_main_loop
bu işlevle ilk bağımsız değişkende bir geri arama, ikinci bağımsız değişkende bir FPS (0
yerel yenileme aralığı için) ve sonsuz döngünün simüle edilip edilmeyeceğini gösteren bir boole (true
) üçüncüde:
emscripten_set_main_loop(callback, 0, true);
Yeni oluşturulan geri arama, yığın değişkenlerine herhangi bir erişime sahip olmayacaktır. main
işlev, yani değişkenler gibi window
Ve renderer
yığın tahsisli bir yapıya çıkarılması ve işaretçisinin üzerinden geçirilmesi gerekir. emscripten_set_main_loop_arg
API’nin varyantı veya global olarak çıkarılan static
değişkenler (basitlik için ikincisiyle gittim). Sonucu takip etmek biraz daha zor ama son örnektekiyle aynı dikdörtgeni çiziyor:
#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 olduğundan ve kaynak koduna yansıtıldığından, Asyncify özelliği olmadan tekrar derlenebilir:
emcc example.cpp -o example.html -s USE_SDL=2
Bu örnek işe yaramaz görünebilir, çünkü kodu çok daha basit olmasına rağmen dikdörtgenin tuval üzerine başarılı bir şekilde çizildiği ilk sürümden hiçbir farkı yoktur. SDL_QUIT
olay—bu alanda işlenen tek olay handle_events
function—web’de zaten yoksayılır.
Ancak, uygun olay döngüsü entegrasyonu – ya Asyncify aracılığıyla ya da aracılığıyla emscripten_set_main_loop
– herhangi bir animasyon veya etkileşim eklemeye karar verirseniz karşılığını verir.
Kullanıcı etkileşimlerini yönetme #
Örneğin, son örnekte birkaç değişiklik yaparak klavye olaylarına yanıt olarak dikdörtgenin hareket etmesini sağlayabilirsiniz:
#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 başka şekiller çizme #
SDL2, platformlar arası farklılıkları ve çeşitli medya aygıtı türlerini tek bir API’de soyutlar, ancak yine de oldukça düşük seviyeli bir kitaplıktır. Özellikle grafikler için noktalar, çizgiler ve dikdörtgenler çizmek için API’ler sağlarken, daha karmaşık şekillerin ve dönüşümlerin uygulanması kullanıcıya bırakılmıştır.
SDL2_gfx bu boşluğu dolduran ayrı bir kitaplıktır. Örneğin, yukarıdaki örnekte bir dikdörtgeni daire ile 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. 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
Ve işte Linux’ta çalışan sonuçlar:
Ve web’de:
Daha fazla grafik ilkel için şuraya göz atın: otomatik oluşturulan dokümanlar.