บทความ › พัฒนาเกม

WebGPU สำหรับเกมบนเว็บ ปี 2026: compute shader ทำ particle และ fallback ไป WebGL

WebGPU ใช้ได้ใน Chrome, Edge, Firefox และ Safari แล้ว แต่ยังมีช่องว่างบน Linux และ Android บทความนี้เขียน particle ด้วย compute shader และวางทางถอยไป WebGL 2

ขั้นสูง อ่าน 12 นาที 23 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • WebGPU เปิดเป็นค่าเริ่มต้นใน Chrome/Edge (Windows, macOS, ChromeOS, Android 12+), Firefox 141 บน Windows, Firefox บน Mac ชิป Apple Silicon และ Safari 26 ทุกแพลตฟอร์มของ Apple
  • ช่องว่างที่เหลือ ณ ตุลาคม 2026: Firefox บน Linux, Android และ Mac ชิป Intel, Chrome บน Linux บางการ์ดจอ และ Windows ARM64
  • compute shader อัปเดต particle หลายแสนตัวบน GPU โดยข้อมูลไม่ต้องวิ่งกลับมาที่ JavaScript ทุกเฟรม
  • ตรวจทั้ง navigator.gpu และ adapter ที่ได้ ถ้าตัวใดตัวหนึ่งไม่มี ให้ถอยไป WebGL 2

WebGPU คือ API กราฟิกและการคำนวณรุ่นใหม่ของเว็บ สิ่งที่ WebGL ไม่มีและเกมได้ประโยชน์ตรง ๆ คือ compute shader ให้ GPU ทำงานคำนวณทั่วไป เช่น particle, ฟิสิกส์แบบง่าย, หรือ culling โดยไม่ต้องหลอกผ่าน fragment shader ปลายปี 2025 web.dev ประกาศว่าเบราว์เซอร์หลักทุกตัวรองรับแล้ว บทความนี้บอกว่า “รองรับ” แปลว่าอะไรจริง ๆ เขียน particle ด้วย compute shader และวางทางถอยไป WebGL 2

ข้อมูลตรวจกับบทความ web.dev (25 พ.ย. 2025), หน้า Implementation Status ของ gpuweb (แก้ไขล่าสุด 2 ต.ค. 2026), MDN และสเปก WebGPU ของ W3C ณ ตุลาคม 2026 โค้ดเขียนตามตัวอย่างใน MDN และสเปก

“ทุกเบราว์เซอร์” แปลว่าอะไร

เบราว์เซอร์เปิดเป็นค่าเริ่มต้นยังไม่ได้ / ต้องเปิด flag
Chrome / EdgeWindows, macOS, ChromeOS ตั้งแต่ 113; Android 12+ (ARM/Qualcomm/Intel) ตั้งแต่ 121; Linux บน Intel Gen12+ ตั้งแต่ 144 และ NVIDIA บน Wayland ตั้งแต่ 147Linux แบบอื่น, Windows ARM64
FirefoxWindows ตั้งแต่ 141; Mac ชิป Apple Silicon: macOS 26 ตั้งแต่ 145 และ macOS ทุกรุ่นตั้งแต่ 147Mac ชิป Intel และ Linux (มีแค่ Nightly), Android (flag)
SafarimacOS, iOS, iPadOS, visionOS 26—

อ่านตารางนี้แบบคนทำเกม คือ ผู้เล่นส่วนใหญ่บนเดสก์ท็อปและ iPhone ได้ WebGPU แต่ผู้เล่น Firefox บน Linux, Android และ Mac ชิป Intel รวมถึง Android รุ่นเก่ากว่า 12 ยังไม่ได้ เกมที่เปิดสาธารณะจึงยังต้องมีทางถอย

“รองรับ” ไม่ได้แปลว่า “ทุกเครื่อง” navigator.gpu มีอยู่ แต่ requestAdapter() ยังคืน null ได้ ถ้าการ์ดจอหรือไดรเวอร์อยู่ใน blocklist

ควรเริ่มใช้ตอนนี้ไหม

ตัดสินจากงานที่เกมทำ ไม่ใช่จากความใหม่ของ API

สถานการณ์คำแนะนำ
เกม 2D หรือ 3D ธรรมดา ใช้ engine อยู่แล้วเปิด backend WebGPU ของ engine ถ้ามี แล้วปล่อยให้ถอยเอง ไม่ต้องเขียน API ตรง
ต้องการ particle หรือ simulation จำนวนมากคุ้มที่จะเขียน compute shader เอง พร้อมเส้นทาง CPU ที่จำนวนน้อยกว่า
กลุ่มผู้เล่นหลักใช้ Linux หรือ Android รุ่นเก่าทำ WebGL 2 เป็นหลัก WebGPU เป็นของเสริม
เครื่องมือภายใน หรือเดโมที่คุมเบราว์เซอร์ได้ใช้ WebGPU อย่างเดียวได้

ภาษา shader ของ WebGPU คือ WGSL ไม่ใช่ GLSL ถ้าย้ายจาก WebGL ต้องเขียน shader ใหม่ หรือใช้ engine ที่แปลงให้ ข้อนี้มักเป็นงานที่ใหญ่ที่สุดของการย้าย มากกว่าตัว API ฝั่ง JavaScript

ตรวจความพร้อมให้ครบสองชั้น

// คืน { device } ถ้าใช้ WebGPU ได้, คืน null ถ้าต้องถอยไป WebGL 2
async function getGPU() {
  if (!("gpu" in navigator)) return null;          // เบราว์เซอร์ไม่มี API หรือไม่ใช่ HTTPS
  const adapter = await navigator.gpu.requestAdapter().catch(() => null);
  if (!adapter) return null;                       // มี API แต่การ์ดจอ/ไดรเวอร์ใช้ไม่ได้
  const device = await adapter.requestDevice();
  device.lost.then((info) => console.warn("GPU device lost:", info.message));
  return { device };
}

device.lost สำคัญกับเกม แท็บที่อยู่เบื้องหลังนาน ไดรเวอร์รีเซ็ต หรือเสียบจอใหม่ ทำให้ device หายได้ ต้องสร้างใหม่และอัปโหลดข้อมูลใหม่

Particle ด้วย compute shader

แนวคิด: เก็บ particle ทั้งหมดใน storage buffer บน GPU ทุกเฟรมสั่ง compute shader อัปเดตตำแหน่งและความเร็ว แล้วใช้ buffer เดิมวาดต่อ ข้อมูลไม่ต้องกลับมาที่ JavaScript

const WGSL = /* wgsl */ `
struct Particle { pos: vec2f, vel: vec2f };
@group(0) @binding(0) var<storage, read_write> ps: array<Particle>;
@group(0) @binding(1) var<uniform> dt: f32;

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
  let i = id.x;
  if (i >= arrayLength(&ps)) { return; }   // กัน thread ส่วนเกินของ workgroup สุดท้าย
  var p = ps[i];
  p.vel.y -= 9.8 * dt;
  p.pos += p.vel * dt;
  if (p.pos.y < -1.0) { p.pos.y = -1.0; p.vel.y = -p.vel.y * 0.5; }  // ตกพื้นแล้วเด้ง
  ps[i] = p;
}`;

function createParticles(device, N) {
  const init = new Float32Array(N * 4);            // pos.x, pos.y, vel.x, vel.y
  for (let i = 0; i < N; i++) {
    init[i * 4 + 2] = Math.random() * 2 - 1;
    init[i * 4 + 3] = Math.random() * 4;
  }
  const particles = device.createBuffer({
    size: init.byteLength,
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(particles, 0, init);

  const params = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  const pipeline = device.createComputePipeline({
    layout: "auto",
    compute: { module: device.createShaderModule({ code: WGSL }), entryPoint: "main" },
  });
  const bindGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
      { binding: 0, resource: { buffer: particles } },
      { binding: 1, resource: { buffer: params } },
    ],
  });

  return function step(dt) {
    device.queue.writeBuffer(params, 0, new Float32Array([dt, 0, 0, 0]));
    const enc = device.createCommandEncoder();
    const pass = enc.beginComputePass();
    pass.setPipeline(pipeline);
    pass.setBindGroup(0, bindGroup);
    pass.dispatchWorkgroups(Math.ceil(N / 64));
    pass.end();
    device.queue.submit([enc.finish()]);
    return particles;                               // ส่งต่อให้ render pass ใช้เป็น vertex buffer
  };
}

render pass วาด particle จาก buffer เดียวกัน (instanced draw อ่าน pos เป็น attribute) ไม่ได้ใส่ไว้เพื่อให้โค้ดสั้น ค่า dt ควรมาจาก game loop แบบ fixed timestep จะได้ particle ตกเร็วเท่ากันทุกจอ

ตัวอย่างในบทนี้100,000 ตัวเพดานต่อ dispatch4.19 ล้านเพดานต่อ buffer8.39 ล้านตามค่า limit เริ่มต้นของสเปก WebGPU
จำนวน particle ต่อหนึ่ง dispatch และหนึ่ง binding ตามค่า limit เริ่มต้น

ตัวอย่างคำนวณ: 100,000 ตัวใช้อะไรเท่าไร

struct Particle มี vec2f สองตัว ตัวละ 8 ไบต์ = 16 ไบต์ต่อตัว

  1. หน่วยความจำ: 100,000 × 16 B = 1,600,000 B ≈ 1.6 MB
  2. จำนวน workgroup: 100,000 ÷ 64 = 1,562.5 → ปัดขึ้น 1,563
  3. thread ที่สั่งรันจริง: 1,563 × 64 = 100,032 → เกิน 32 ตัว นี่คือเหตุผลที่ต้องมี if (i >= arrayLength(&ps))

เพดานจากค่า limit เริ่มต้นในสเปก (อุปกรณ์จริงอาจสูงกว่า ขอเพิ่มได้ตอน requestDevice)

  • maxComputeWorkgroupsPerDimension = 65,535 → dispatch มิติเดียวรันได้ 65,535 × 64 = 4,194,240 ตัว เกินนั้นต้องใช้มิติ y หรือ dispatch หลายรอบ
  • maxStorageBufferBindingSize = 128 MiB = 134,217,728 B → ÷ 16 = 8,388,608 ตัว ต่อ binding
  • maxComputeInvocationsPerWorkgroup = 256 → @workgroup_size(64) อยู่ในงบสบาย

เทียบกับวิธีเก่าบน WebGL: อัปเดตบน CPU แล้วอัปโหลด 1.6 MB ขึ้น GPU ทุกเฟรม ที่ 60 fps = 1.6 × 60 = 96 MB/s ผ่าน bufferSubData บวกกับลูป JavaScript 100,000 รอบต่อเฟรม

ทางถอยไป WebGL 2

มีสองทางที่ใช้จริง

1. ใช้ engine ที่ถอยให้เอง WebGPURenderer ของ three.js ลอง WebGPU ก่อน ถ้าไม่ได้จะใช้ backend WebGL 2 และตั้ง forceWebGL: true เพื่อทดสอบเส้นทางนี้บนเครื่องที่มี WebGPU ได้ web.dev ระบุว่า Babylon.js, PlayCanvas และ Unity รองรับ WebGPU ด้วย

2. เขียนสองเส้นทางเอง ให้ logic ของ particle เป็นสูตรเดียวกัน แต่รันคนละที่ ถ้าไม่มี WebGPU ใช้ CPU กับจำนวนที่น้อยลง

const gpu = await getGPU();
const N = gpu ? 100_000 : 10_000;                  // ฝั่ง CPU ลดจำนวนลงสิบเท่า

function stepCPU(buf, dt) {                         // สูตรเดียวกับ WGSL ทุกบรรทัด
  for (let i = 0; i < buf.length; i += 4) {
    buf[i + 3] -= 9.8 * dt;
    buf[i] += buf[i + 2] * dt;
    buf[i + 1] += buf[i + 3] * dt;
    if (buf[i + 1] < -1) { buf[i + 1] = -1; buf[i + 3] = -buf[i + 3] * 0.5; }
  }
}

ลดจำนวน particle บนเส้นทางสำรองเป็นเรื่องปกติ ผู้เล่นได้เกมที่เล่นได้ แค่ฟุ้งน้อยลง ดีกว่าจอดำ

ข้อผิดพลาดที่เจอบ่อย

  • ตรวจแค่ navigator.gpu แล้วไม่เช็ก adapter เครื่องที่ GPU ถูก blocklist จะพังตอน requestDevice
  • ทดสอบบน http ผ่าน IP ในวงแลน API หายเพราะไม่ใช่ secure context ใช้ localhost หรือ HTTPS
  • ลืมกัน thread ส่วนเกิน เขียนเลยท้าย array ใน compute shader
  • อ่านผลกลับมา JavaScript ทุกเฟรม mapAsync รอ GPU ทำให้เสียประโยชน์ของ compute ทั้งหมด ใช้ buffer ต่อบน GPU
  • ไม่รับมือ device.lost แท็บกลับมาแล้วเกมค้าง ต้องสร้าง device และ buffer ใหม่
  • ทดสอบแค่ Chrome บนเครื่องตัวเอง เปิด Firefox บน Linux หรือใช้ forceWebGL ดูเส้นทางสำรองทุกครั้งก่อนปล่อย

แหล่งอ้างอิง

คำถามที่พบบ่อย

เปิด WebGPU บน http ธรรมดาได้ไหม

ไม่ได้ ทั้ง API ใช้ได้เฉพาะ secure context คือ HTTPS หรือ localhost ตอนพัฒนา ถ้า navigator.gpu เป็น undefined บนเซิร์ฟเวอร์ทดสอบ ให้ตรวจเรื่องนี้ก่อน

ใช้ three.js อยู่ ต้องเขียน fallback เองไหม

ไม่ต้อง WebGPURenderer ของ three.js ลอง WebGPU ก่อน ถ้าเบราว์เซอร์ไม่รองรับจะถอยไป backend WebGL 2 เอง และมี forceWebGL ให้บังคับทดสอบเส้นทาง WebGL ได้

WebGPU เร็วกว่า WebGL เสมอไหม

ไม่เสมอ ฉากธรรมดาที่ GPU ไม่ใช่คอขวดอาจต่างกันน้อย จุดที่ WebGPU ได้เปรียบชัดคือ compute shader และการลดงาน CPU ต่อ draw call วัดกับเกมของตัวเองก่อนสรุป

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#เกมกลาง

Godot 4.6 ใช้ Jolt เป็นฟิสิกส์ 3D ค่าเริ่มต้น: โปรเจกต์ เก่าต้องย้ายเองไหม

Godot 4.6 ตั้ง Jolt Physics ให้โปรเจกต์ 3D ใหม่ แต่โปรเจกต์เดิมยังใช้ GodotPhysics3D อยู่ บทความนี้อธิบายพฤติกรรมที่ต่าง ค่าที่ต้องรู้ และวิธีย้ายพร้อมทดสอบ

อ่าน 8 นาที
#เกมกลาง

Roblox Server Authority: กันโกงด้วย client prediction และ rollback

Roblox เปิด Server Authority ให้ทุกเกมแล้ว เซิร์ฟเวอร์เป็นเจ้าของสถานะเกม เครื่องผู้เล่นทายผลล่วงหน้าแล้วย้อนแก้เมื่อทายผิด วิธีเปิด วิธีเขียนโค้ด และข้อจำกัด

อ่าน 10 นาที
#เกมกลาง

ทำไมเกมวิ่งเร็วขึ้นบนจอ 144Hz: game loop แบบ fixed timestep และ interpolation

เกมที่ขยับตามจำนวนเฟรมจะเร็วขึ้นบนจอรีเฟรชสูง แก้ด้วย fixed timestep + accumulator, interpolation ตอนวาด และเพดานกัน spiral of death พร้อมตัวเลข 60/144 Hz และโค้ด JS

อ่าน 11 นาที