ภาพวาดบนผืนผ้าใบในรูปแบบ Emscripten

ดูวิธีแสดงผลกราฟิก 2 มิติบนเว็บจาก WebAssembly ด้วย Emscripten

ระบบปฏิบัติการแต่ละระบบมี API ในการวาดกราฟิกแตกต่างกัน แต่จะยิ่งสับสนมากขึ้นเมื่อเขียนโค้ดข้ามแพลตฟอร์มหรือพอร์ตกราฟิกจากระบบหนึ่งไปยังอีกระบบหนึ่ง รวมถึงเมื่อพอร์ตโค้ดแบบเนทีฟไปยัง WebAssembly

ในโพสต์นี้ คุณจะได้เรียนรู้วิธีสองสามวิธีในการวาดกราฟิก 2 มิติลงใน Canvas เอลิเมนต์บนเว็บจากโค้ด C หรือ C++ ที่คอมไพล์ด้วย Emscripten

Canvas ผ่าน Embind

หากคุณกำลังเริ่มต้นโปรเจ็กต์ใหม่แทนที่จะพอร์ตโปรเจ็กต์ที่มีอยู่ วิธีที่ง่ายที่สุดอาจเป็นการใช้ Canvas API ของ HTML ผ่านระบบการเชื่อมโยง Embind ของ Emscripten Embind ช่วยให้คุณดำเนินการกับค่า JavaScript ที่กำหนดเองได้โดยตรง

หากต้องการทำความเข้าใจวิธีใช้ Embind ให้ดูตัวอย่างจาก MDN ต่อไปนี้ซึ่งพบองค์ประกอบ <canvas> และวาดรูปร่างบางส่วนลงบนองค์ประกอบนั้น

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'green';
ctx.fillRect(10, 10, 150, 100);

วิธีถอดเสียงเป็น C++ ด้วย Embind มีดังนี้

#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);
}

เมื่อลิงก์โค้ดนี้ โปรดส่ง --bind เพื่อเปิดใช้ Embind

emcc --bind example.cpp -o example.html

จากนั้นคุณสามารถแสดงเนื้อหาที่รวบรวมไว้ด้วยเซิร์ฟเวอร์แบบคงที่และโหลดตัวอย่างในเบราว์เซอร์ได้โดยทำดังนี้

หน้า HTML ที่สร้างขึ้นด้วย Emscript ซึ่งแสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสีดำ

การเลือกองค์ประกอบ Canvas

เมื่อใช้ Shell HTML ที่สร้างโดย Emscripten กับคำสั่ง Shell ก่อนหน้า ระบบจะรวม Canvas ไว้และตั้งค่าให้คุณ คุณสามารถสร้างการสาธิตและตัวอย่างแบบง่ายๆ ได้ง่ายขึ้น แต่ในแอปพลิเคชันขนาดใหญ่ คุณควรรวม JavaScript และ WebAssembly ที่สร้างโดย Emscripten ไว้ในหน้า HTML ที่คุณออกแบบเอง

โค้ด JavaScript ที่สร้างขึ้นควรพบเอลิเมนต์ Canvas ที่จัดเก็บไว้ในพร็อพเพอร์ตี้ Module.canvas คุณสามารถตั้งค่าได้ในระหว่างการเริ่มต้นเช่นเดียวกับคุณสมบัติอื่นๆ ของโมดูล

หากคุณใช้โหมด ES6 (การตั้งค่าเอาต์พุตไปยังเส้นทางที่มีส่วนขยาย .mjs หรือใช้การตั้งค่า -s EXPORT_ES6) คุณสามารถส่ง Canvas ได้ดังนี้

import initModule from './emscripten-generated.mjs';

const Module = await initModule({
  canvas: document.getElementById('my-canvas')
});

หากใช้เอาต์พุตสคริปต์ปกติ คุณต้องประกาศออบเจ็กต์ Module ก่อนโหลดไฟล์ JavaScript ที่สร้างโดย Emscripten ดังนี้

<script>
var Module = {
  canvas: document.getElementById('my-canvas')
};
</script>
<script src="emscripten-generated.js"></script>

OpenGL และ SDL2

OpenGL เป็น API แบบข้ามแพลตฟอร์มที่ได้รับความนิยมสำหรับกราฟิกคอมพิวเตอร์ เมื่อใช้ใน Emscripten การดำเนินการย่อยของ OpenGL ที่รองรับจะเปลี่ยนเป็น WebGL หากแอปพลิเคชันของคุณใช้ฟีเจอร์ที่รองรับใน OpenGL ES 2.0 หรือ 3.0 แต่ไม่ได้รองรับใน WebGL ทาง Emscripten ก็สามารถจำลองฟีเจอร์เหล่านั้นได้เช่นกัน แต่คุณต้องเลือกใช้ผ่านการตั้งค่าที่เกี่ยวข้อง

คุณสามารถใช้ OpenGL โดยตรงหรือผ่านไลบรารีกราฟิก 2 มิติและ 3 มิติระดับสูงขึ้นก็ได้ และมี 2 อย่างที่ถ่ายโอนไปยังเว็บด้วย Emscripten ในโพสต์นี้ เราจะมุ่งเน้นที่กราฟิก 2 มิติ และปัจจุบัน SDL2 เป็นไลบรารีที่แนะนำสำหรับงานดังกล่าว เนื่องจากผ่านการทดสอบมาเป็นอย่างดีและรองรับแบ็กเอนด์ Emscripten อย่างเป็นทางการ

การวาดสี่เหลี่ยมผืนผ้า

ส่วน "เกี่ยวกับ SDL" ในเว็บไซต์อย่างเป็นทางการระบุว่า

Simple DirectMedia Layer เป็นไลบรารีการพัฒนาข้ามแพลตฟอร์มที่ออกแบบมาเพื่อมอบสิทธิ์เข้าถึงระดับต่ำสำหรับเสียง แป้นพิมพ์ เมาส์ จอยสติ๊ก และฮาร์ดแวร์กราฟิกผ่าน OpenGL และ Direct3D

ฟีเจอร์ดังกล่าวทั้งหมด ไม่ว่าจะเป็นการควบคุมเสียง แป้นพิมพ์ เมาส์ และกราฟิก ได้รับการย้ายมาและใช้งานร่วมกับ Emscripten บนเว็บได้เช่นกัน คุณจึงสามารถพอร์ตเกมทั้งหมดที่สร้างด้วย SDL2 ได้โดยไม่ต้องยุ่งยาก หากคุณพอร์ตโปรเจ็กต์ที่มีอยู่ โปรดดูส่วน"การผสานรวมกับระบบบิลด์" ในเอกสาร Emscripten

เพื่อความง่ายดาย ในโพสต์นี้เราจะมุ่งเน้นที่กรณีไฟล์เดียวและแปลตัวอย่างสี่เหลี่ยมผืนผ้าก่อนหน้านี้เป็น SDL2

#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 คุณต้องใช้ -s USE_SDL=2 การดำเนินการนี้จะบอกให้ Emscripten ดึงข้อมูลไลบรารี SDL2 ซึ่งคอมไพล์ไปยัง WebAssembly ไว้ล่วงหน้าแล้ว และลิงก์กับแอปพลิเคชันหลัก

emcc example.cpp -o example.html -s USE_SDL=2

เมื่อโหลดตัวอย่างในเบราว์เซอร์ คุณจะเห็นสี่เหลี่ยมผืนผ้าสีเขียวที่คุ้นเคยดังต่อไปนี้

หน้า HTML ที่สร้างโดย Emscript ซึ่งแสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

อย่างไรก็ตาม โค้ดนี้มีปัญหาบางประการ ประการแรก ทรัพยากรที่จัดสรรไม่มีการล้างข้อมูลทรัพยากรที่จัดสรรอย่างเหมาะสม ประการที่ 2 ในเว็บ หน้าเว็บจะไม่ปิดโดยอัตโนมัติเมื่อแอปพลิเคชันทำงานเสร็จสิ้นแล้ว ดังนั้นรูปภาพบนผืนผ้าใบจะยังคงอยู่ อย่างไรก็ตาม เมื่อคอมไพล์โค้ดเดียวกันอีกครั้งแบบเนทีฟด้วย

clang example.cpp -o example -lSDL2

และดำเนินการแล้ว หน้าต่างที่สร้างขึ้นจะกะพริบเพียงครู่เดียวและปิดทันทีเมื่อออก ผู้ใช้จึงไม่มีเวลาเห็นรูปภาพ

การผสานรวมลูปเหตุการณ์

ตัวอย่างที่สมบูรณ์และมีสำนวนมากกว่านี้จะต้องรอในลูปเหตุการณ์จนกว่าผู้ใช้เลือกที่จะออกจากแอปพลิเคชัน

#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();
}

หลังจากวาดรูปภาพไปยังหน้าต่างแล้ว ตอนนี้แอปพลิเคชันจะรอเป็นวงกลม ซึ่งสามารถประมวลผลเหตุการณ์เกี่ยวกับแป้นพิมพ์ เมาส์ และเหตุการณ์อื่นๆ ของผู้ใช้ได้ เมื่อผู้ใช้ปิดหน้าต่าง ระบบจะเรียกเหตุการณ์ SDL_QUIT ซึ่งจะขัดจังหวะเพื่อออกจากลูป หลังจากออกจากลูปแล้ว แอปพลิเคชันจะล้างและออกจากการทำงานเอง

ตอนนี้การรวมตัวอย่างนี้บน Linux ทำงานได้ตามที่คาดไว้ และแสดงหน้าต่างขนาด 300 x 300 ที่มีสี่เหลี่ยมผืนผ้าสีเขียว:

หน้าต่างสี่เหลี่ยมจัตุรัสของ Linux ที่มีพื้นหลังสีดําและสี่เหลี่ยมผืนผ้าสีเขียว

แต่ตัวอย่างนี้ใช้งานบนเว็บไม่ได้อีกต่อไป หน้าที่สร้างขึ้นด้วย Emscript จะค้างทันทีระหว่างการโหลดและจะไม่แสดงรูปภาพที่แสดงผล

หน้า HTML ที่ Emscripten สร้างขึ้นซึ่งวางซ้อนกับกล่องโต้ตอบข้อผิดพลาด &quot;หน้าไม่ตอบสนอง&quot; ที่แนะนำให้รอให้หน้าเว็บกลับมาทำงานอีกครั้งหรือออกจากหน้า

เกิดอะไรขึ้น เราจะยกคำตอบจากบทความ"การใช้ Web API แบบไม่สอดคล้องจาก WebAssembly" ดังนี้

สรุปสั้นๆ คือเบราว์เซอร์จะเรียกใช้โค้ดทั้งหมดในลักษณะของลูปที่ไม่มีที่สิ้นสุด โดยดึงโค้ดจากคิวทีละรายการ เมื่อมีการทริกเกอร์เหตุการณ์บางอย่าง เบราว์เซอร์จะจัดคิวตัวแฮนเดิลที่เกี่ยวข้อง และในการวนรอบครั้งถัดไป ระบบจะนำเหตุการณ์ออกจากคิวและดำเนินการ กลไกนี้ช่วยให้จำลองการเกิดขึ้นพร้อมกันและเรียกใช้การดำเนินการพร้อมกันจำนวนมากขณะใช้เพียงเทรดเดียว

สิ่งที่สําคัญที่ควรทราบเกี่ยวกับกลไกนี้คือ ขณะที่โค้ด JavaScript (หรือ WebAssembly) ที่กำหนดเองทำงานอยู่ ระบบจะบล็อกลูปเหตุการณ์ […]

ตัวอย่างก่อนหน้านี้จะเรียกใช้ลูปเหตุการณ์แบบไม่สิ้นสุด ขณะที่โค้ดเองจะทำงานภายในลูปเหตุการณ์แบบไม่สิ้นสุดอีกรายการหนึ่งซึ่งเบราว์เซอร์ระบุไว้โดยนัย ลูปภายในจะไม่ละทิ้งการควบคุมไปยังด้านนอก ดังนั้นเบราว์เซอร์จึงไม่มีโอกาสประมวลผลเหตุการณ์ภายนอกหรือดึงสิ่งต่างๆ ลงในหน้า

การแก้ปัญหานี้ทำได้ 2 วิธี

การเลิกบล็อก Event Loop ด้วย Asyncify

ก่อนอื่น คุณสามารถใช้ Asyncify ตามที่อธิบายไว้ในบทความที่ลิงก์ โดยเป็นฟีเจอร์ Emscripten ที่ช่วยให้ "หยุดชั่วคราว" โปรแกรม C หรือ C++, ให้การควบคุมกลับไปยังลูปเหตุการณ์ และปลุกโปรแกรมเมื่อการทำงานแบบไม่พร้อมกันบางอย่างเสร็จสิ้น

การดำเนินการแบบอะซิงโครนัสดังกล่าวอาจแม้กระทั่ง "นอนหลับเป็นเวลาขั้นต่ำที่เป็นไปได้" ซึ่งแสดงผ่าน emscripten_sleep(0) API การฝังไว้ตรงกลางของลูปช่วยให้มั่นใจได้ว่าระบบจะส่งการควบคุมกลับไปยังลูปเหตุการณ์ของเบราว์เซอร์ในการวนซ้ำแต่ละครั้ง และหน้าเว็บจะยังคงตอบสนองและจัดการเหตุการณ์ต่างๆ ได้

#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();
}

ตอนนี้ต้องคอมไพล์โค้ดนี้โดยเปิดใช้ Asyncify

emcc example.cpp -o example.html -s USE_SDL=2 -s ASYNCIFY

และแอปพลิเคชันก็ทํางานตามที่คาดไว้บนเว็บอีกครั้ง

หน้า HTML ที่ Emscripten สร้างขึ้นซึ่งแสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดํา

อย่างไรก็ตาม Asyncify อาจมีโอเวอร์เฮดของขนาดโค้ดที่ไม่สำคัญ หากใช้สำหรับลูปเหตุการณ์ระดับบนสุดในแอปพลิเคชันเท่านั้น ตัวเลือกที่ดีกว่าคือการใช้ฟังก์ชัน emscripten_set_main_loop

เลิกบล็อกลูปเหตุการณ์ด้วย API "ลูปหลัก"

emscripten_set_main_loop ไม่จำเป็นต้องใช้การเปลี่ยนรูปแบบคอมไพเลอร์สำหรับการเลิกทำและย้อนกลับสแต็กการเรียก ซึ่งจะช่วยหลีกเลี่ยงค่าใช้จ่ายเพิ่มเติมเกี่ยวกับขนาดโค้ด แต่ในทางกลับกัน ก็ต้องแก้ไขโค้ดด้วยตนเองเป็นอย่างมาก

ก่อนอื่น คุณต้องแยกเนื้อหาของลูปเหตุการณ์ออกเป็นฟังก์ชันแยกต่างหาก จากนั้น จะต้องเรียก emscripten_set_main_loop ด้วยฟังก์ชันนั้นเป็น Callback ในอาร์กิวเมนต์แรก, FPS ในอาร์กิวเมนต์ที่ 2 (0 สำหรับช่วงเวลาการรีเฟรชดั้งเดิม) และบูลีนที่ระบุว่าจะจำลองลูปอนันต์ (true) ในอาร์กิวเมนต์ที่ 3 หรือไม่

emscripten_set_main_loop(callback, 0, true);

ฟังก์ชันการเรียกกลับที่สร้างขึ้นใหม่จะไม่มีสิทธิ์เข้าถึงตัวแปรสแต็กในฟังก์ชัน main ดังนั้นจึงต้องดึงตัวแปรอย่าง window และ renderer ไปยังโครงสร้างที่ระบบจัดสรรหน่วยความจำกองซ้อน และส่งผ่านพอยน์เตอร์ผ่านตัวแปร emscripten_set_main_loop_arg ของ API หรือดึงไปยังตัวแปร static ระดับส่วนกลาง (เราเลือกวิธีหลังเพื่อความสะดวก) ผลลัพธ์จะยากขึ้นเล็กน้อย แต่จะวาดสี่เหลี่ยมผืนผ้าเหมือนในตัวอย่างสุดท้าย

#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();
}

เนื่องจากการเปลี่ยนแปลงขั้นตอนการควบคุมทั้งหมดเป็นการดำเนินการด้วยตนเอง และจะปรากฏในซอร์สโค้ด จึงสามารถคอมไพล์การเปลี่ยนแปลงได้โดยไม่ต้องใช้ฟีเจอร์ Asyncify อีกครั้ง

emcc example.cpp -o example.html -s USE_SDL=2

ตัวอย่างนี้อาจดูไร้ประโยชน์เพราะใช้งานได้ไม่แตกต่างจากเวอร์ชันแรกซึ่งวาดรูปสี่เหลี่ยมผืนผ้าบนผืนผ้าใบได้สำเร็จ แม้ว่าโค้ดจะง่ายกว่ามาก และเหตุการณ์ SDL_QUIT ซึ่งเป็นเหตุการณ์เดียวที่จัดการในฟังก์ชัน handle_events ก็ยังคงไม่สนใจบนเว็บ

อย่างไรก็ตาม การผสานรวม Event Loop ที่เหมาะสม ไม่ว่าจะผ่าน Asyncify หรือผ่าน emscripten_set_main_loop จะใช้ได้ผลหากคุณตัดสินใจเพิ่มภาพเคลื่อนไหวหรือการโต้ตอบประเภทใดก็ตาม

การจัดการการโต้ตอบของผู้ใช้

เช่น เมื่อทำการเปลี่ยนแปลงเล็กน้อยในตัวอย่างสุดท้าย คุณจะทำให้รูปสี่เหลี่ยมผืนผ้าเลื่อนตามเหตุการณ์บนแป้นพิมพ์ได้

#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

SDL2 ตัดความแตกต่างข้ามแพลตฟอร์มและอุปกรณ์สื่อประเภทต่างๆ ออกไปใน API เดียว แต่ก็ยังเป็นไลบรารีอยู่ในระดับที่ค่อนข้างต่ำ โดยเฉพาะด้านกราฟิก มี API สำหรับวาดจุด เส้น และสี่เหลี่ยมผืนผ้า แต่ผู้ใช้ยังคงใช้งานรูปทรงและการเปลี่ยนรูปแบบที่ซับซ้อนยิ่งขึ้นได้

SDL2_gfx เป็นไลบรารีแยกต่างหากที่เติมเต็มช่องว่างดังกล่าว เช่น สามารถใช้แทนสี่เหลี่ยมผืนผ้าในตัวอย่างด้านบนด้วยวงกลม

#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();
}

ตอนนี้ต้องลิงก์ไลบรารี SDL2_gfx เข้ากับแอปพลิเคชันด้วย ซึ่งทำงานคล้ายกับ SDL2:

# 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 มีดังนี้

หน้าต่างสี่เหลี่ยมจัตุรัสของ Linux ที่มีพื้นหลังสีดําและวงกลมสีเขียว

และบนเว็บ

หน้า HTML ที่สร้างโดย Emscript ซึ่งแสดงวงกลมสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

สำหรับกราฟิกพื้นฐานเพิ่มเติม โปรดดูเอกสารที่สร้างขึ้นโดยอัตโนมัติ