1 điểm bởi GN⁺ 23 ngày trước | 1 bình luận | Chia sẻ qua WhatsApp
  • 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>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()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 đồgóc nhìn bám theo ở ngôi thứ ba
  • Dùng các hàm CSS sin()cos() để tính vị trí camera phía sau người chơi
  • Tách riêng các thuộc tính rotatetranslate để 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

    • Nhưng rồi có lẽ ngay cả hệ thống đẩy mà họ làm ra cuối cùng cũng sẽ chạy được DOOM
  • Đâ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 đó

    • Đây là một ví dụ điển hình của đảo ngược trừu tượng (abstraction inversion)
      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
    • Điểm cốt lõi là ranh giới giữa trình bày (CSS)tương tác (JavaScript) nằm ở đâu
      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

    • Nhưng CPU x86 bằng CSS quá chậm để xử lý game loop. Cuối cùng vẫn cần JS
    • Sự tiến hóa này của CSS là kết quả đã được báo trước, và tôi nghĩ phe HTML ngay từ đầu lẽ ra nên chọn DSSSL
  • 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ẹo
    Ví dụ như dùng animation-delay@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ậy

  • Hai mã cheat DOOM là IDDQDIDKFA 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 div

    • div à, trước cả thời đó còn có giai đoạn mọi thứ đều làm bằng table layout nữa
  • Thật sự rất ấn tượng! Chỉ cần xóa một div là đã có thể xuyên tường (wall hack)

    • Thậm chí nếu đặt .wall thành opacity: 0.7, còn có thể tái hiện đúng kiểu hack tường trong suốt ngày xưa
  • Tôi đã tự hỏi “có thể tự chạy thử cái này ở đâu?”, và hóa ra là ở cssdoom.wtf

    • Vừa chạy trên điện thoại là máy nóng lên ngay
    • Đây là lần đầu tôi thấy DOOM chạy mượt thế này trên di động
    • Nó còn hoạt động hoàn hảo trên Safari — chuyện này gần như hiếm khi xảy ra
    • Trên Firefox thì chạy tốt, nhưng phím Alt lại bị map để mở rồi đóng menu nên khá bất tiện
      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”

    • Cũng có người đáp lại rằng có khi bạn chỉ đọc tiêu đề rồi vào bình luận thôi chăng
  • 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