1 คะแนน โดย GN⁺ 2026-03-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • โปรเจกต์ทดลองที่ เรนเดอร์ 3D DOOM ด้วย CSS เพียงอย่างเดียว โดยสร้างกำแพงและอ็อบเจ็กต์ทั้งหมดด้วย <div> และ 3D transform
  • ลอจิกของเกมทำงานด้วย JavaScript แต่ การเรนเดอร์ทำด้วย CSS ทั้งหมด เพื่อสำรวจขีดจำกัดของเบราว์เซอร์และ CSS สมัยใหม่
  • ใช้ความสามารถ CSS สมัยใหม่ เช่น ตรีโกณมิติ, clip-path, @property, SVG filter, anchor positioning เพื่อสร้างทั้งกำแพง พื้น แสง สไปรต์ และเอฟเฟ็กต์ระเบิด
  • เนื่องจาก CSS ไม่มีแนวคิดเรื่องกล้อง จึงจัดการมุมมองด้วยวิธี ขยับโลกแทนผู้เล่น และควบคุมการเคลื่อนที่ทั้งหมดผ่านการอัปเดต custom property
  • แม้ประสิทธิภาพจะไม่ถึงระดับ WebGL แต่ก็เป็นกรณีที่พิสูจน์ ศักยภาพในการขยายความสามารถด้านการแสดงผลและการคำนวณของ CSS

การเรนเดอร์ 3D DOOM ด้วย CSS

  • โปรเจกต์ทดลองที่ เรนเดอร์ DOOM ด้วย CSS เพียงอย่างเดียว โดยกำแพง พื้น และอ็อบเจ็กต์ทั้งหมดสร้างจาก <div> และวางตำแหน่งด้วย 3D transform
    • ลอจิกของเกมรันใน JavaScript แต่การเรนเดอร์ทั้งหมดทำโดย CSS
    • เป้าหมายของโปรเจกต์คือ สำรวจขีดจำกัดของเบราว์เซอร์และ CSS สมัยใหม่

กลับไปสู่คณิตศาสตร์ระดับมัธยมปลาย

  • ดึงข้อมูลจากไฟล์ WAD ของ DOOM ต้นฉบับ (vertices, linedefs, sidedefs, sectors) เพื่อประกอบฉากแบบสแตติกด้วย <div> หลายพันชิ้น
  • กำแพงแต่ละด้านรับพิกัดจุดเริ่มต้น-จุดสิ้นสุดและความสูงของพื้น-เพดานผ่าน CSS custom property
  • ใช้ฟังก์ชัน CSS hypot() และ atan2() เพื่อคำนวณความยาวและมุมหมุนของกำแพง
  • JavaScript ส่งข้อมูลดิบให้ และ CSS เป็นฝ่ายคำนวณตรีโกณมิติเพื่อเรนเดอร์
  • game loop กับ renderer แยกจากกัน โดย JS รับผิดชอบเพียงจัดการสถานะและอัปเดตพิกัด

ปัญหาการแปลงระบบพิกัด

  • DOOM ใช้ระบบพิกัด 2D ที่ค่า Y เพิ่มขึ้นไปทางทิศเหนือ แต่ CSS 3D ใช้ Y ชี้ขึ้นด้านบน และ Z ชี้เข้าหาผู้ชม
  • ตอนแปลงใช้รูปแบบ translate3d(x,-z,-y) เพื่อให้ระบบพิกัดตรงกัน
  • จุดเด่นคือการคำนวณ rotateY(atan2(var(--delta-y), var(--delta-x))) ทำงานได้โดยไม่ต้องมีการแปลงเพิ่ม

ขยับโลกแทนกล้อง

  • เพราะ CSS ไม่มีแนวคิดเรื่องกล้อง จึงใช้วิธี ขยับโลกในทิศตรงกันข้ามแทนผู้เล่น
  • JS อัปเดตเพียง custom property 4 ตัวคือ --player-x/y/z/angle
  • ใช้ translate: 0 0 var(--perspective) เพื่อชดเชยมุมมอง และใช้ rotateY กับ translate3d สำหรับการหมุนและย้ายตำแหน่งมุมมอง
  • การเคลื่อนที่ทั้งหมด จัดการผ่านการอัปเดต property เท่านั้น

พื้นคือ div ที่นอนราบ

  • โดยปกติองค์ประกอบ DOM จะเป็นระนาบแนวตั้ง ดังนั้นพื้นจึงถูกวางให้นอนราบด้วย rotateX(90deg)
  • ใช้ clip-path, polygon(), path() เพื่อแสดงพื้นที่หลายเหลี่ยมที่ซับซ้อนและช่องโหว่ต่าง ๆ
  • ฟังก์ชัน shape() ใน CSS รุ่นใหม่ทำให้ใช้เส้นทางแบบอิงเปอร์เซ็นต์ร่วมกับกฎ evenodd ได้

การจัดแนวเท็กซ์เจอร์

  • เพื่อไม่ให้เท็กซ์เจอร์ขาดตอนระหว่าง sector ที่อยู่ติดกัน จึงใช้ background-position อิงพิกัดโลก
  • ทุก sector ใช้กริดเท็กซ์เจอร์เดียวกัน จึงเชื่อมขอบต่อกันได้อย่าง ลื่นไหล

ประตู ลิฟต์ และแอนิเมชันด้วย @property

  • การเปิดประตูคือการยกเพดานของ sector ขึ้น โดยจัดการ transform ของ <div> คอนเทนเนอร์ด้วย CSS transition
  • ลิฟต์ต้องพาผู้เล่นเคลื่อนที่ไปด้วย จึงให้ JS ซิงก์ค่า --player-z
  • ใช้ @property เพื่อ ลงทะเบียน custom property เป็นค่าตัวเลข ทำให้เกิดเอฟเฟ็กต์ตกและเคลื่อนที่อย่างลื่นไหล

สไปรต์และการกลับด้าน

  • สไปรต์ของศัตรูใช้วิธี billboard ที่หันเข้าหากล้องตลอดเวลา
  • จาก 8 ทิศทาง ใช้ภาพจริงเพียง 5 ชุด ส่วนที่เหลือจัดการด้วย การกลับซ้ายขวา (scaleX)
  • ใช้แอนิเมชัน steps() สำหรับสลับเฟรมเดิน โจมตี และตาย
  • ปัญหาศัตรูทุกตัวเดินพร้อมกันแก้ด้วย animation-delay แบบสุ่ม จาก JS

โปรเจกไทล์ ระเบิด และเอฟเฟ็กต์กระสุน

  • จรวด ลูกไฟ และสิ่งอื่น ๆ ใช้ CSS animation เพื่อ เคลื่อนที่จาก A ไป B อัตโนมัติ
  • JS ตั้งค่าเพียงพิกัดเริ่มต้น-สิ้นสุดและระยะเวลา จากนั้นเมื่อตรวจชนก็ลบองค์ประกอบและสร้างสไปรต์ระเบิด
  • เอฟเฟ็กต์ระเบิดและควันกระสุนจะถูกลบอัตโนมัติหลังแอนิเมชัน 3 เฟรมแบบ steps()

แสงและฟิลเตอร์

  • กำหนดค่าความสว่างราย sector ด้วย property --light และให้องค์ประกอบภายในสืบทอดผ่าน filter: brightness()
  • แสงกระพริบทำโดยเปลี่ยนค่า --light เป็นช่วง ๆ ด้วย @keyframes
  • ศัตรูโปร่งใส (Spectre) แสดงเป็นเงาบิดเบี้ยวด้วย SVG filter (feColorMatrix, feTurbulence, feDisplacementMap)

UI แบบ responsive และ anchor positioning

  • เกมรองรับ อุปกรณ์พกพา โดย HUD ใช้ flex-wrap เพื่อขึ้นบรรทัดใหม่
  • สไปรต์อาวุธปรับตำแหน่งอัตโนมัติตามความสูงของ HUD ด้วย anchor-name / position-anchor
  • ปุ่มควบคุมแบบสัมผัสก็จัดวางด้วยวิธี anchor แบบเดียวกัน

โหมดผู้ชม

  • รองรับทั้ง มุมมองภาพรวมทั้งแผนที่ และ มุมมองติดตามบุคคลที่สาม
  • ใช้ฟังก์ชัน CSS sin() และ cos() เพื่อคำนวณตำแหน่งกล้องด้านหลังผู้เล่น
  • แยก property rotate และ translate ออกจากกันเพื่อให้ เปลี่ยนมุมมองได้อย่างลื่นไหล
  • JS อัปเดตเพียงตำแหน่งและมุม ส่วนคณิตศาสตร์ของกล้องให้ CSS จัดการ

การคัดทิ้งและประสิทธิภาพ

  • องค์ประกอบ 3D หลายพันชิ้นทำให้เกิด ภาระต่อ compositor ของเบราว์เซอร์
  • การคัดทิ้งแบบ JS: ตั้งค่าองค์ประกอบที่อยู่นอกมุมมองเป็น hidden
  • การทดลองคัดทิ้งแบบ CSS: ควบคุม visibility ด้วยค่าที่คำนวณได้ โดยใช้เทคนิค type grinding
  • หากฟังก์ชัน if() ถูกทำให้เป็นมาตรฐาน ก็จะสามารถแทนที่ด้วยเงื่อนไขที่กระชับกว่าได้

การจัดเรียงตามความลึก

  • เบราว์เซอร์ จัดการลำดับความลึก (z-order) ให้อัตโนมัติ
  • วัตถุที่อยู่บนระนาบเดียวกันจะใส่ offset เล็กน้อย เพื่อป้องกันการกะพริบ

“ลูกเล่นหลอกตา” ของ DOOM และการจัดการท้องฟ้า

  • DOOM ต้นฉบับใช้ เทคนิคฉายภาพ โดยวาดท้องฟ้าเป็นเท็กซ์เจอร์ 2D บน “กำแพง”
  • แต่ตัวเรนเดอร์ CSS ต้องวางท้องฟ้าไว้ในพื้นที่ 3D จริง จึงเกิดปัญหา มองเห็นด้านหลังของแผนที่ ในบางฉาก
  • วิธีแก้คือระหว่างขั้นตอนคัดทิ้ง ให้ ตัดองค์ประกอบด้านหลังผนังท้องฟ้าออกจากการเรนเดอร์

บทสรุป — ขีดจำกัดและความเป็นไปได้ของ CSS

  • game loop ทั้งหมดอยู่ใน JS ส่วนการเรนเดอร์แยกออกมาเป็น CSS ล้วน
  • ใช้ความสามารถ CSS สมัยใหม่อย่าง ตรีโกณมิติ, @property, clip-path, SVG filter, anchor positioning จนถึงขีดสุด
  • แม้ประสิทธิภาพจะไม่ถึงระดับ WebGL แต่ก็พิสูจน์ ศักยภาพในการขยายขอบเขตการแสดงผลของ CSS
  • พบทั้งบั๊กด้าน 3D และปัญหาประสิทธิภาพจำนวนมากใน Safari และ Chrome
  • บทสรุปสุดท้าย: “เราสามารถรัน DOOM ด้วย CSS ได้หรือไม่?” → ทำได้ Yes, it can.

1 ความคิดเห็น

 
GN⁺ 2026-03-29
ความคิดเห็นจาก Hacker News
  • คนประเภทที่ชอบพูดว่า “เอาสิ่งนี้ไปรัน DOOM มาแล้ว” ควรถูกจ้างเข้ากรม ระบบขับเคลื่อนอวกาศ ของรัฐบาล
    พวกเขาเป็นคนที่ต้องมี โจทย์ประหลาดแบบสุด ๆ ให้ทำ จะได้ไม่ใช่แค่นั่งขยับนิ้วไปวัน ๆ

    • แต่สุดท้ายดูเหมือนแม้แต่ระบบขับเคลื่อนที่พวกเขาสร้าง ก็คงจะทำให้มันรัน DOOM ได้อยู่ดี
  • นี่ดูเป็นโปรเจกต์แนว “ทำได้ก็เลยทำ”
    เดิมที CSS เป็นภาษาสำหรับจัดสไตล์แบบประกาศเจตนา แต่ตอนนี้มันค่อย ๆ กลายเป็น ระบบที่โปรแกรมได้ มากขึ้น ด้วยการมีทั้งเงื่อนไข ฟังก์ชันคณิตศาสตร์ และลูกเล่นด้านการเรนเดอร์
    ประเด็นสำคัญไม่ใช่ว่า “จะรัน DOOM ด้วย CSS ได้ไหม” แต่คือเรากำลัง ยัดตรรกะเข้าไปในเลเยอร์ ที่เดิมไม่ได้ถูกออกแบบมาสำหรับสิ่งนี้มากแค่ไหน

    • นี่คือตัวอย่างคลาสสิกของ abstraction inversion
      CSS ซ่อนความอยากเป็นภาษาโปรแกรมไว้ แต่สุดท้ายมันก็กลายเป็น abstraction ที่ผิดที่ผิดทางไปหมด
    • ประเด็นหลักคือเส้นแบ่งระหว่าง การแสดงผล (CSS) กับ การโต้ตอบ (JavaScript) อยู่ตรงไหนกันแน่
      เมื่อก่อนเราต้องใช้ JS สำหรับดรอปดาวน์ ทูลทิป และเลย์เอาต์ แต่ตอนนี้ใน CSS สามารถกำหนดได้ทั้ง anchor positioning และเงื่อนไขอย่าง if()
      แม้แต่แอนิเมชัน การสลับรายละเอียด และเอฟเฟกต์ด้านการเข้าถึงก็ทำได้ด้วย CSS
  • การสร้างฉาก 3D ด้วย CSS นั้นทำได้มานานแล้ว แต่ถ้าจะให้โต้ตอบได้ก็ยังต้องพึ่ง JS
    ตอนนี้มีอย่าง โครงการ x86CSS ที่สามารถ อีมูเลต CPU ด้วย CSS ล้วน ๆ โดยไม่ใช้ JS
    เลยทำให้สงสัยว่า DOOM จะสามารถทำงานแบบเรียลไทม์ด้วย CSS ล้วนได้หรือไม่

    • แต่มีคนบอกว่า CPU x86 บน CSS ช้าเกินไปจนจัดการ game loop ไม่ได้ สุดท้ายก็ยังต้องใช้ JS
    • วิวัฒนาการของ CSS แบบนี้ถือเป็น ผลลัพธ์ที่คาดเดาได้อยู่แล้ว และคิดว่าฝั่ง HTML น่าจะเลือก DSSSL ตั้งแต่แรก
  • กรณีนี้แสดงให้เห็นชัดว่าทำไมผู้คนถึงอยากได้ CSS ที่อิงกับ TypeScript
    เพราะฟีเจอร์อย่าง if() ที่ใช้ได้เฉพาะใน Chrome ทำให้นักพัฒนาต้องใช้ลูกเล่นแบบนี้
    ยกตัวอย่างเช่น ใช้ animation-delay กับ @keyframes เพื่อเลียนแบบการ สลับการมองเห็น
    ถ้า CSS if() กลายเป็นมาตรฐาน ก็จะจัดการเงื่อนไขได้สะอาดขึ้นโดยไม่ต้องแฮ็กแบบนี้

  • โค้ดโกง DOOM อย่าง IDDQD และ IDKFA น่าเสียดายที่ใช้ไม่ได้

  • ทำให้นึกถึงสมัยก่อนที่ถ้าจะทำมุมโค้งให้ div ต้องใช้ GIF สี่ภาพ

    • div เหรอ ก่อนหน้านั้นยังมียุคที่ทุกอย่างทำด้วย table layout เลย
  • น่าประทับใจจริง ๆ! แค่ลบ div ตัวเดียวก็ทำ wall hack ได้แล้ว

    • ไปไกลกว่านั้น ถ้าใส่ opacity: 0.7 ให้ .wall ก็จะได้ความรู้สึกแบบ transparent wallhack ยุคเก่ากลับมาเป๊ะ ๆ
  • ตอนแรกก็สงสัยว่า “มีที่ไหนให้ลองเล่นเองไหม?” แล้วก็พบว่าลองได้ที่ cssdoom.wtf

    • เปิดบนมือถือปุ๊บ เครื่องร้อน เลย
    • นี่เป็นครั้งแรกที่เห็น DOOM รันบนมือถือได้ลื่นขนาดนี้
    • มันทำงานได้สมบูรณ์แม้แต่บน Safari — ซึ่งแทบไม่เคยเกิดขึ้น
    • บน Firefox มันทำงานได้ดี แต่ การแมปปุ่ม Alt ดันไปเปิดปิดเมนู เลยใช้งานลำบาก
      ส่วนบน Chromium กลับกระตุกกว่า และก็หา ปุ่ม strafing ไม่เจอ
      ถึงอย่างนั้นโดยรวมก็ยังเป็นงานที่น่าทึ่งมาก
  • CSS เป็นสเปกตัวอย่างชั้นดีที่แสดงให้เห็น ข้อจำกัดของการออกแบบโดยคณะกรรมการ
    มันแข่งกับ SVG ในตำแหน่ง “สเปกที่ดูประหลาดที่สุด”

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