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

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

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

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

Canvas ผ่าน Embind

หากคุณเริ่มโปรเจ็กต์ใหม่แทนที่จะพยายามพอร์ตโปรเจ็กต์ที่มีอยู่ วิธีที่อาจเป็นวิธีที่ง่ายที่สุดในการใช้ HTML Canvas API ผ่านระบบการเชื่อมโยงของ Emscripten Embind 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

เมื่อใช้เชลล์ 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 ซึ่งแสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

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

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; กล่องโต้ตอบที่แสดงข้อผิดพลาด ซึ่งแนะนำให้รอให้หน้าดังกล่าวรับผิดชอบหรือออกจากหน้าเว็บ

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

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

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

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

ปัญหานี้มี 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 ที่สร้างโดย Emscript ซึ่งแสดงสี่เหลี่ยมผืนผ้าสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

อย่างไรก็ตาม 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);

Callback ที่สร้างขึ้นใหม่จะไม่มีสิทธิ์เข้าถึงตัวแปรสแต็กในฟังก์ชัน 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 ซึ่งแสดงวงกลมสีเขียวบนผืนผ้าใบสี่เหลี่ยมจัตุรัสสีดำ

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