- Một thử nghiệm kết xuất 3D DOOM chỉ bằng CSS, trong đó mọi bức tường và đối tượng đều được dựng bằng
<div> và biến đổi 3D (transform)
- Logic game do JavaScript đảm nhiệm, nhưng việc kết xuất hoàn toàn do CSS thực hiện, nhằm khám phá giới hạn của trình duyệt và CSS hiện đại
- Tận dụng các tính năng CSS mới như lượng giác, clip-path, @property, bộ lọc SVG, định vị neo để dựng tường, sàn, ánh sáng, sprite và cả hiệu ứng nổ
- Do CSS không có khái niệm camera, góc nhìn được xử lý bằng cách di chuyển thế giới thay vì người chơi, và mọi chuyển động đều được điều khiển bằng cách cập nhật thuộc tính tùy biến
- Không đạt hiệu năng như WebGL, nhưng đây là một ví dụ chứng minh khả năng mở rộng về sức biểu đạt và tính toán của CSS
Kết xuất 3D DOOM bằng CSS
- Một dự án thử nghiệm kết xuất DOOM chỉ bằng CSS, trong đó toàn bộ tường, sàn và đối tượng đều được tạo bằng
<div> và sắp xếp bằng biến đổi 3D (transform)
- Logic game chạy trong JavaScript, nhưng phần kết xuất hoàn toàn do CSS đảm nhiệm
- Mục tiêu của dự án là khám phá giới hạn của trình duyệt và CSS hiện đại
Quay lại toán trung học
- Trích xuất dữ liệu tệp WAD của DOOM gốc (vertices, linedefs, sidedefs, sectors) để dựng một cảnh tĩnh gồm hàng nghìn
<div>
- Mỗi bức tường nhận tọa độ đầu-cuối và độ cao sàn-trần qua thuộc tính tùy biến CSS
- Dùng các hàm CSS
hypot() và atan2() để tính chiều dài tường và góc xoay
- JavaScript truyền dữ liệu thô, còn CSS tự tính toán lượng giác để thực hiện kết xuất
- Vòng lặp game và renderer được tách rời, JS chỉ phụ trách quản lý trạng thái và cập nhật tọa độ
Bài toán chuyển đổi hệ tọa độ
- DOOM dùng hệ tọa độ 2D với trục Y tăng về phía bắc, trong khi CSS 3D có Y hướng lên trên và Z hướng về phía người xem
- Khi chuyển đổi, dùng dạng
translate3d(x,-z,-y) để khớp hệ tọa độ
- Điểm đáng chú ý là phép tính
rotateY(atan2(var(--delta-y), var(--delta-x))) hoạt động mà không cần biến đổi bổ sung
Di chuyển thế giới thay vì camera
- Vì CSS không có khái niệm camera, nên sử dụng cách di chuyển thế giới theo hướng ngược lại thay vì di chuyển người chơi
- JS chỉ cập nhật bốn thuộc tính tùy biến
--player-x/y/z/angle
- Dùng
translate: 0 0 var(--perspective) để hiệu chỉnh góc nhìn, và rotateY cùng translate3d để xoay tầm nhìn và di chuyển vị trí
- Mọi chuyển động đều được xử lý chỉ bằng cập nhật thuộc tính
Sàn là một div được đặt nằm xuống
- Vì phần tử DOM mặc định là một mặt phẳng đứng, sàn được đặt nằm ngang bằng
rotateX(90deg)
- Dùng
clip-path, polygon(), path() để biểu diễn các vùng đa giác phức tạp và các lỗ hổng
- Hàm
shape() của CSS mới cho phép dùng đường dẫn theo phần trăm cùng với quy tắc evenodd
Căn chỉnh texture
- Để texture không bị đứt giữa các sector liền kề, sử dụng
background-position dựa trên tọa độ thế giới
- Mọi sector cùng chia sẻ một lưới texture thống nhất để tạo sự nối liền mượt mà ở ranh giới
Cửa, thang nâng và hoạt ảnh @property
- Hiệu ứng mở cửa được thực hiện bằng cách nâng trần của sector lên, và xử lý
transform của <div> chứa bằng chuyển tiếp CSS (transition)
- Với thang nâng, vì người chơi di chuyển cùng nó nên JS phải đồng bộ
--player-z
- Dùng
@property để đăng ký thuộc tính tùy biến dưới dạng số, từ đó tạo hiệu ứng rơi và di chuyển mượt mà
Sprite và lật gương
- Sprite của kẻ địch luôn quay mặt về phía camera theo kiểu billboard
- Trong 8 hướng chỉ có 5 bộ ảnh là thật, các hướng còn lại được xử lý bằng lật ngang (
scaleX)
- Chuyển khung hình đi bộ, tấn công và chết bằng hoạt ảnh
steps()
- Vấn đề mọi kẻ địch cùng bước đi một lúc được giải quyết bằng
animation-delay ngẫu nhiên trong JS
Đạn bay, vụ nổ và hiệu ứng đạn
- Tên lửa, cầu lửa... được xử lý bằng hoạt ảnh CSS để tự động di chuyển từ A đến B
- JS chỉ thiết lập tọa độ đầu-cuối và thời lượng; khi va chạm thì xóa phần tử và tạo sprite vụ nổ
- Vụ nổ và khói đạn sẽ tự bị xóa sau hoạt ảnh 3 khung hình dựa trên
steps()
Ánh sáng và bộ lọc
- Mỗi sector được gán độ sáng qua thuộc tính
--light, và các phần tử bên trong kế thừa bằng filter: brightness()
- Đèn nhấp nháy được tạo bằng
@keyframes để thay đổi giá trị --light theo chu kỳ
- Kẻ địch tàng hình một phần (Spectre) được thể hiện bằng bộ lọc SVG (
feColorMatrix, feTurbulence, feDisplacementMap) để tạo bóng méo dạng
Giao diện phản hồi và định vị neo
- Trò chơi hỗ trợ di động, HUD dùng
flex-wrap để tự xuống dòng
- Sprite vũ khí được tự động căn theo chiều cao HUD bằng
anchor-name / position-anchor
- Các nút điều khiển cảm ứng cũng được bố trí bằng cùng cơ chế neo này
Chế độ quan sát
- Hỗ trợ quan sát toàn bộ bản đồ và góc nhìn bám theo ở ngôi thứ ba
- Dùng các hàm CSS
sin() và cos() để tính vị trí camera phía sau người chơi
- Tách riêng các thuộc tính
rotate và translate để tạo chuyển đổi góc nhìn mượt mà
- JS chỉ cập nhật vị trí và góc, còn phần toán học camera do CSS xử lý
Culling và hiệu năng
- Hàng nghìn phần tử 3D gây ra tải nặng cho compositor của trình duyệt
- Culling bằng JS: đặt các phần tử ngoài tầm nhìn thành
hidden
- Thử nghiệm culling bằng CSS: điều khiển
visibility bằng giá trị tính toán, sử dụng mẹo type grinding
- Nếu hàm
if() được chuẩn hóa, có thể thay bằng các biểu thức điều kiện gọn hơn
Sắp xếp độ sâu
- Trình duyệt tự động xử lý sắp xếp độ sâu (z-order)
- Các đối tượng trên cùng một mặt phẳng được thêm độ lệch rất nhỏ để tránh nhấp nháy
“Mánh khóe” của DOOM và cách xử lý bầu trời
- DOOM gốc dùng một mẹo chiếu hình để vẽ bầu trời như một “bức tường” bằng texture 2D phía trên
- Vì renderer CSS phải đặt bầu trời trong không gian 3D thực, nên ở một số cảnh xuất hiện vấn đề lộ phần phía sau bản đồ
- Cách khắc phục là ở bước culling, loại khỏi kết xuất các phần tử nằm sau tường bầu trời
Kết luận — giới hạn và khả năng của CSS
- Toàn bộ vòng lặp game chạy bằng JS, còn phần kết xuất được tách riêng và xây dựng thuần bằng CSS
- Các tính năng CSS hiện đại như lượng giác, @property, clip-path, bộ lọc SVG, định vị neo được khai thác đến giới hạn
- Dù không đạt hiệu năng cấp WebGL, dự án vẫn chứng minh khả năng mở rộng sức biểu đạt của CSS
- Đồng thời phát hiện nhiều lỗi 3D và vấn đề hiệu năng trên Safari và Chrome
- Kết luận cuối cùng: “Có thể chạy DOOM bằng CSS không?”
→ Có thể. Yes, it can.
1 bình luận
Ý kiến trên Hacker News
Tôi nghĩ những người kiểu “tôi đã chạy cái này bằng DOOM” nên được tuyển vào bộ phận hệ thống đẩy vũ trụ của chính phủ
Họ là kiểu người cần những bài toán kỳ lạ chứ không thể chỉ ngồi nghịch ngón tay
Đây giống kiểu dự án “làm chỉ vì có thể làm được”
CSS vốn ban đầu là một ngôn ngữ tạo kiểu khai báo, nhưng giờ đã có cả điều kiện, hàm toán học và các mẹo render, nên đang dần biến thành một hệ thống có thể lập trình
Điều quan trọng không phải là “có thể chạy DOOM bằng CSS không”, mà là chúng ta đang nhồi bao nhiêu logic vào một lớp vốn không được thiết kế cho mục đích đó
CSS đang cố che giấu ham muốn trở thành ngôn ngữ lập trình, nhưng rốt cuộc lại biến thành một dạng trừu tượng hoàn toàn sai lệch
Trước đây phải cần JS cho dropdown, tooltip và layout, nhưng giờ trong CSS thậm chí có thể chỉ định vị trí neo hay điều kiện như
if()Animation, bật/tắt chi tiết, thậm chí cả các hiệu ứng liên quan đến accessibility giờ cũng có thể xử lý bằng CSS
Việc tạo cảnh 3D bằng CSS vốn đã có thể làm từ lâu, nhưng tương tác thì vẫn cần JS
Giờ đây, như dự án x86CSS, người ta còn có thể mô phỏng CPU chỉ bằng CSS mà không cần JS
Vì thế cũng tò mò liệu DOOM có thể được hiện thực theo thời gian thực bằng CSS thuần hay không
Trường hợp này cho thấy rất rõ vì sao mọi người lại muốn CSS dựa trên TypeScript
Vì những tính năng như
if()chỉ chạy trên Chrome, các lập trình viên phải dùng đủ loại mẹoVí dụ như dùng
animation-delayvà@keyframesđể giả lập bật/tắt hiển thịNếu CSS
if()được chuẩn hóa, sẽ có thể xử lý điều kiện gọn gàng mà không cần các kiểu hack như vậyHai mã cheat DOOM là IDDQD và IDKFA tiếc là không hoạt động
Điều này làm tôi nhớ lại thời phải dùng bốn ảnh GIF chỉ để tạo góc bo tròn cho một
divdivà, trước cả thời đó còn có giai đoạn mọi thứ đều làm bằng table layout nữaThật sự rất ấn tượng! Chỉ cần xóa một
divlà đã có thể xuyên tường (wall hack).wallthànhopacity: 0.7, còn có thể tái hiện đúng kiểu hack tường trong suốt ngày xưaTôi đã tự hỏi “có thể tự chạy thử cái này ở đâu?”, và hóa ra là ở cssdoom.wtf
Trên Chromium thì ngược lại còn giật hơn, và tôi không tìm ra phím strafe
Dù vậy nhìn chung đây vẫn là một bản hiện thực đáng kinh ngạc
CSS là một đặc tả tiêu biểu cho giới hạn của thiết kế bởi ủy ban
Cùng với SVG, nó đang cạnh tranh ngôi “đặc tả xấu xí nhất từng thấy”
Nói thêm một điều về bản hiện thực tuyệt vời này,
thực ra không phải người chơi di chuyển mà là cả thế giới đang di chuyển
Camera chỉ là một công cụ mang tính khái niệm để tính góc nhìn (
frustum) mà thôi