WebGPU สำหรับเกมบนเว็บ ปี 2026: compute shader ทำ particle และ fallback ไป WebGL
WebGPU ใช้ได้ใน Chrome, Edge, Firefox และ Safari แล้ว แต่ยังมีช่องว่างบน Linux และ Android บทความนี้เขียน particle ด้วย compute shader และวางทางถอยไป WebGL 2
- 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 / Edge | Windows, macOS, ChromeOS ตั้งแต่ 113; Android 12+ (ARM/Qualcomm/Intel) ตั้งแต่ 121; Linux บน Intel Gen12+ ตั้งแต่ 144 และ NVIDIA บน Wayland ตั้งแต่ 147 | Linux แบบอื่น, Windows ARM64 |
| Firefox | Windows ตั้งแต่ 141; Mac ชิป Apple Silicon: macOS 26 ตั้งแต่ 145 และ macOS ทุกรุ่นตั้งแต่ 147 | Mac ชิป Intel และ Linux (มีแค่ Nightly), Android (flag) |
| Safari | macOS, 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 ตัวใช้อะไรเท่าไร
struct Particle มี vec2f สองตัว ตัวละ 8 ไบต์ = 16 ไบต์ต่อตัว
- หน่วยความจำ: 100,000 × 16 B = 1,600,000 B ≈ 1.6 MB
- จำนวน workgroup: 100,000 ÷ 64 = 1,562.5 → ปัดขึ้น 1,563
- 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 ตัว ต่อ bindingmaxComputeInvocationsPerWorkgroup= 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 วัดกับเกมของตัวเองก่อนสรุป
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ