2 điểm bởi GN⁺ 2024-05-03 | 1 bình luận | Chia sẻ qua WhatsApp
  • ShapeUp ra đời từ thử thách “nhìn lại phần mềm hiện có dưới một góc nhìn mới” của Wheel Reinvention Jam, và được hoàn thiện thành một trình dựng mô hình 3D có demo chạy trên trình duyệt cùng khả năng xuất .obj
  • Điểm mấu chốt giúp tạo công cụ 3D trong vòng một tuần là ray marched SDF; so với renderer dựa trên tam giác, nó cho phép nhanh chóng triển khai cảnh có màu sắc, bóng mềm và ambient occlusion
  • Phần triển khai được giữ đơn giản, chủ yếu xoay quanh một tệp C duy nhất; mô hình lưu tối đa 100 Shape trong mảng tĩnh để giảm gánh nặng quản lý bộ nhớ
  • raylib giúp mở nhanh cửa sổ OpenGL, nhưng do API thiên về int, thiếu kiểm tra tham số, phụ thuộc GLFW và các giới hạn của raygui, tác giả phải tự dùng OpenGL hoặc tự viết lại chức năng
  • Kết quả cuối cùng gồm 2024 dòng C và 250 dòng GLSL, tổng cộng khoảng 2300 dòng, hỗ trợ mở/lưu tệp, chạy trên nhiều nền tảng và xuất .obj

Quá trình ShapeUp trở thành trình dựng mô hình 3D

  • Wheel Reinvention Jam là một sự kiện lập trình kéo dài một tuần nhằm nhìn lại các hệ thống phần mềm hiện có dưới một góc nhìn mới
  • Mục tiêu ban đầu xuất phát từ sự khó chịu với trình biên dịch TypeScript chậm: tạo một tập con TypeScript nhanh hơn tsc
    • Việc dùng parser TypeScript của esbuild hoặc Bun làm điểm khởi đầu có vẻ khả thi
    • Nhưng demo thành công chỉ là “một lệnh terminal kết thúc nhanh hơn lệnh khác”, không hấp dẫn về mặt thị giác, nên cuối cùng hướng đi được chuyển sang 3D
  • ShapeUp được tạo thành một trình dựng mô hình 3D cho phép chỉnh sửa hình khối bằng chuột
    • Trước đó đã có kinh nghiệm viết shader SDF, nhưng cách mô hình hóa bằng việc trực tiếp sửa code không tự nhiên
    • Mục tiêu là cho phép chỉnh sửa hình dạng dựa trên SDF bằng chuột

Vì sao SDF giúp dự án một tuần trở nên khả thi

  • Nền tảng render của ShapeUp là ray marched signed distance fields(SDFs)
  • Cảnh SDF có thể được triển khai nhanh hơn renderer dựa trên tam giác, ngay cả khi bao gồm màu sắc, bóng mềm và ambient occlusion
  • Ví dụ Inigo Quilez tạo nhân vật phong cách Pixar ngay tại chỗ bằng SDF trở thành điểm tham chiếu kỹ thuật cho hướng đi này
  • ShapeUp xử lý mô hình hóa SDF bằng cách thao tác trực tiếp với hình khối thay vì chỉnh sửa code

Triển khai bằng C và cấu trúc dữ liệu

  • ShapeUp được viết bằng C và dùng raylib để tạo cửa sổ OpenGL
  • C được chọn vì biên dịch nhanh, cú pháp không che giấu các hành vi phức tạp, quen thuộc, và có khả năng biên dịch cho cả native lẫn WebAssembly
  • Mô hình được cấu thành từ một tập hợp các struct Shape
    • Mỗi Shape có vị trí, kích thước, góc, bán kính góc bo, mức độ blob, màu sắc, trục mirror và cờ subtract
  • Danh sách Shape được quản lý bằng mảng tĩnh thay vì cấp phát động
    • MAX_SHAPE_COUNT là 100
    • Trạng thái được quản lý bằng Shape shapes[MAX_SHAPE_COUNT], shape_count, selected_shape
    • Cách này loại bỏ khả năng cấp phát thất bại và rò rỉ bộ nhớ
  • Giới hạn 100 Shape không phải vấn đề lớn trong sử dụng thực tế
    • Do thiếu thời gian tối ưu renderer, framerate đã giảm trước khi chạm tới 100 Shape
    • Nếu có thời gian, tác giả định chia mô hình thành các khối gạch nhỏ và thực hiện ray marching bên trong từng khối

Cách sử dụng bộ nhớ

  • ShapeUp chỉ dùng cấp phát bộ nhớ động ở 3 nơi
    • Lưu: cấp phát buffer đủ chứa toàn bộ tài liệu
    • Xuất .OBJ: cấp phát buffer đủ chứa toàn bộ đỉnh
    • Tạo GLSL shader: cấp phát buffer cho mã nguồn shader
  • Trong mỗi trường hợp, chỉ gọi free một lần ở cuối hàm
  • Cũng có thể malloc từng Shape và lưu con trỏ trong mảng động, nhưng cấu trúc như vậy không cần thiết cho dự án này
  • C có lợi thế ở khả năng kiểm soát trực tiếp bố cục bộ nhớ
  • Nếu cần mảng động hoặc hashmap, có thể dùng công cụ như stb_ds.h

Cách triển khai UI

  • UI được triển khai theo kiểu immediate mode user interface(IMGUI)
  • Ưu điểm của IMGUI là dễ debug, và có thể quyết định vị trí phần tử bằng ngôn ngữ lập trình thật thay vì CSS, constraints hay SwiftUI
  • Phần tử đang được focus hoặc thao tác chuột được theo dõi bằng enum Control
    • Các trạng thái thao tác như vị trí, kích thước, góc, màu, di chuyển, xoay, scale, xoay camera và mức độ blob được biểu diễn bằng giá trị enum
    • focused_controlmouse_action chứa trạng thái UI hiện tại

Những điểm vướng với raylib và raygui

  • raylib hữu ích để nhanh chóng mở cửa sổ OpenGL, nhưng càng về sau lại trở thành yếu tố làm chậm tốc độ phát triển
  • Phần đặc biệt bất tiện trong API raylib là thiếu thông tin kiểu
    • Ngay cả ở nơi đáng ra cần kiểu enum, API vẫn dùng int, nên không nhận được kiểm tra kiểu từ compiler
    • Chỉ nhìn chữ ký hàm thì khó thấy rõ ý nghĩa tham số
    • Ví dụ, gesture trong IsGestureDetected(unsigned int gesture) trông giống ID gesture đã đăng ký, nhưng thực tế là enum Gesture
    • Tài liệu chủ yếu nằm trong header file, nên muốn biết một int nào đó thực ra có phải enum hay không thì phải xem implementation
  • Thiết kế không kiểm tra tham số cơ bản cũng làm vấn đề lớn hơn
    • LoadFileData(const char *fileName, int * dataSize) sẽ segfault nếu dataSizeNULL
    • Header không cho biết dataSize có phải tham số output hay không, hay có được phép null không
    • Vì thiếu kiểm tra, việc truy vết những vấn đề đơn giản trở nên khó hơn, và trong một số trường hợp có thể âm thầm hoạt động sai
  • Cách xử lý dependency cũng khác kỳ vọng
    • Có vấn đề raylib không workaround hoặc không gửi patch cho các issue của GLFW
    • Với người dùng cuối, việc các tính năng raylib hoạt động đúng quan trọng hơn chi tiết triển khai nội bộ của cách tạo cửa sổ
  • Thư viện UI raygui có quá nhiều hạn chế để dùng cho dự án này
    • Không hiển thị được số dấu phẩy động, nên phải tự tạo trường văn bản cho float
    • Không xử lý được định tuyến sự kiện chuột cho các phần tử chồng lên nhau hoặc bị cắt
    • Không hỗ trợ bo góc, một nhu cầu phổ biến trong UI
    • Khó styling cho đẹp
  • Bug cũng cản trở luồng phát triển
    • Do bug trong công cụ raygui, không thể đổi font mặc định vốn được cách điệu quá mức
    • Các hàm vẽ như DrawCircle(...) không chia sẻ đỉnh giữa các tam giác, nên nếu ma trận hiện tại có scale hoặc rotation thì sai số dấu phẩy động tạo ra khe hở pixel
  • Tác giả đã báo các issue phát hiện được trong một thời gian, nhưng đa số bị đóng là “wont fix”, nên sau đó ngừng báo cáo
  • Cách workaround là tự dùng các hàm OpenGL hoặc tự triển khai từ đầu những chức năng cần thiết
  • Về sau tác giả dự định dùng sokol thay cho raylib

Bốn việc phải hoàn thành trong 6 ngày

  • ShapeUp phải hoàn thành bốn phần lớn trong vòng 6 ngày
    • Giao diện người dùng: gizmo 3D, phím tắt bàn phím, sidebar, game controller
    • Bộ sinh GLSL shader và renderer ray marching
    • Chọn đối tượng bằng chuột dựa trên GPU
    • Marching cubes để xuất file
  • Khó khăn nằm ở việc giữ đúng ưu tiên hơn là bản thân từng tính năng
  • Những vấn đề khó hoặc tốn thời gian được né bằng cách đổi thiết kế, hoặc xử lý bằng giải pháp đơn giản chạy được trong 90% trường hợp
  • Có những tính năng tìm ra lời giải sau khi để sang một ngày
  • Cách làm là luôn duy trì một trình dựng mô hình 3D đang hoạt động, rồi cải thiện dần khi thời gian cho phép
    • Điều này được ví như không xây để chỉ tới thời điểm hoàn tất mới thành kim tự tháp, mà là ở bất kỳ giai đoạn nào dừng lại cũng có một kim tự tháp nhỏ hoàn chỉnh

Kết quả cuối cùng

  • Khi một tuần kết thúc, ShapeUp đã có thể tạo các mô hình 3D có ý nghĩa và xuất ra tệp .obj
  • Chạy trên nhiều nền tảng, đồng thời hỗ trợ mở và lưu tệp
  • Quy mô mã là 2024 dòng C250 dòng GLSL
  • Việc có thể tạo một trình dựng mô hình 3D hữu dụng ở mức nhất định với khoảng 2300 dòng là kết quả đáng chú ý
  • Bản thân dự án tương đối đơn giản, nhưng cảm quan chọn thứ cần làm, kiến thức để làm được, và kỷ luật hoàn thành trong một tuần đã đóng vai trò quan trọng

1 bình luận

 
GN⁺ 2024-05-03
Ý kiến trên Hacker News
  • Tôi hoàn toàn đồng ý với tác giả về các giới hạn của Raylib. Hiện tôi đang làm một game kiểu tower defense bắt đầu bằng Raylib, và đang gặp rất nhiều giới hạn tương tự cùng các vấn đề khác
    Ví dụ như chuyển đổi toàn màn hình không hoạt động nhất quán giữa các nền tảng, không thể liệt kê các chế độ màn hình, khó bật/tắt các tính năng render khi đang chạy, vấn đề lưu shader đã biên dịch, v.v.
    Dù vậy tôi vẫn biết ơn công sức Ray đã bỏ vào thư viện này và vẫn sẽ tiếp tục tài trợ. Raylib rất tuyệt để nhanh chóng tạo prototype, nhưng để đi xa hơn thì không dễ, trừ khi chấp nhận các ràng buộc rất nặng
    Chắc chắn tôi đã học được nhiều điều, nhưng đến giờ thì dự án đã đi quá xa để thay toàn bộ mã liên quan đến Raylib bằng thứ như SDL

    • Chi tiết Raylib không xuất phát từ ray tracing mà từ tên người tạo ra nó, Ray, khá thú vị
      Things Unexpectedly Named After People: https://notes.rolandcrosby.com/posts/unexpectedly-eponymous/
    • Raylib dễ bắt đầu, nhưng khi dự án phức tạp hơn một chút thì nó bắt đầu “cắn” lại. Ngược lại, SDL mất nhiều thời gian thiết lập ban đầu hơn, nhưng mở rộng rất tốt khi dự án lớn lên. Chất lượng mã cũng xuất sắc một cách hiếm có
    • Raylib có nhiều vấn đề có lẽ sẽ không được sửa trong tương lai, nhưng khó có thể đổ riêng vấn đề toàn màn hình cho Raylib. Toàn màn hình trên Windows thực tế đã hỏng đến mức gần như không dùng được suốt hàng chục năm, và các nền tảng khác có thể cũng tương tự
      Chiến lược ngày nay là cứ dùng chế độ cửa sổ không viền và giả vờ như không có toàn màn hình thật
    • Cảm giác tương tự. Khoảng 2 tháng trước tôi bắt đầu một dự án và chọn Raylib; những thứ cơ bản chạy rất dễ, nhưng càng dùng càng liên tục gặp các bất tiện nhỏ nhặt và ngẫu nhiên. Giờ thì tôi đã đầu tư quá nhiều vào dự án để quay lại việc dùng Raylib
      Vấn đề lớn nhất hiện tại là xử lý phông chữ và render văn bản. Có lẽ tôi sẽ phải chuyển từ phông TTF sang phông bitmap dựng sẵn, nhưng khi bản địa hóa về sau chắc sẽ khá khổ
      Hai tính năng tôi nhớ nhất sau khi chuyển từ Love2D là render văn bản nhiều màu một cách dễ dàng, và dễ dàng cắt texture để lặp hoặc lát gạch. Trong Raylib, tôi phải tự tách văn bản theo markup màu, áp dụng offset chiều rộng, rồi còn tính cả xuống dòng để gọi hàm vẽ cho từng mảnh
      Nếu vẽ nhiều chữ lên màn hình thì FPS dường như cũng tụt mạnh, có thể là do batching các lệnh vẽ văn bản bị phá vỡ. Trước đây từng có hàm vẽ texture dạng tile, nhưng vì lý do nào đó đã bị gỡ bỏ
    • Thấy vậy tôi muốn xem thử raylib. Có các ví dụ dễ thương chạy bằng WebAssembly: https://www.raylib.com/examples.html
      Điều luôn khiến tôi khó chịu ở Wasm và đồ họa 3D/2D trên trình duyệt là thường thấy các vấn đề nhỏ như cuộn trang. Hãy xem ví dụ “Background scrolling & parallax” ở đây: https://www.raylib.com/examples.html
      Tôi đã thử trên nhiều thiết bị, và nếu mắt tôi không có vấn đề thì rõ ràng đó không phải là cuộn mượt. Thật khó tin là đến năm 2024, cuộn mượt 2D vẫn chưa phải là vấn đề đã được giải quyết
  • “Các Shape được lưu trong một mảng cấp phát tĩnh. Không lỗi cấp phát, không rò rỉ, không rườm rà. Thật đáng yêu. Giới hạn 100 Shape thực tế không phải là ràng buộc. Vì gần như không có thời gian tối ưu renderer, có lẽ tốc độ khung hình đã giảm trước khi chạm tới 100.”
    Đây là một trong những ví dụ hay nhất gần đây về tránh tối ưu hóa quá sớm

    • Tôi nghĩ gần như ngược lại. Đó là tránh trừu tượng hóa và tổng quát hóa quá sớm
    • Một ví dụ tuyệt vời cho thấy sự khác biệt giữa những người trực tiếp làm ra thứ gì đó và những người chỉ ngồi tranh cãi xem nên làm thế nào
  • Bài viết thật sự thú vị, và tôi thích việc tác giả kể về nhiều quyết định như cách xử lý bộ nhớ và các vấn đề gặp phải với raylib. Đúng lúc tôi đang ôn lại C khi bước vào phần 2 của Crafting Interpreters, nên thật vui khi được nhớ lại những điều C làm tốt

  • Demo thời gian thực trong video thật sự rất hay. Chưa nói đến việc làm ứng dụng, nếu là tôi thử thì chắc ngay cả video đó cũng không làm xong trong một tuần

    • Làm video còn tốn thời gian hơn làm ứng dụng. Tôi không hiểu các YouTuber làm sao mà duy trì đều đặn như vậy
  • Từ rất lâu trước đây tôi từng làm một hệ điều hành cho điện thoại bàn. Vì chỉ có 64K RAM nên hoàn toàn không có quản lý bộ nhớ động, và tôi dùng rất nhiều biến tĩnh để trình biên dịch bố trí tất cả ngay lúc biên dịch
    Ta rất dễ quên rằng nhiều ứng dụng có thể hoàn toàn không cần quản lý bộ nhớ động. Nhiều khi chỉ cần cấp phát vài buffer kích thước cố định và xử lý gọn các tình huống ngoại lệ khi buffer đầy là đủ
    Trong bối cảnh đó, C thực ra an toàn hơn nhiều. Không có rò rỉ bộ nhớ, thứ cần lo chỉ là tràn buffer. Nếu mọi biến đều được cấp phát tĩnh, có thể quản lý bằng cách cẩn thận khi dùng sizeof
    Điều này không có nghĩa Rust và Go ngày nay không phải lựa chọn tuyệt vời, nhưng C cổ điển, giản dị vẫn hoạt động tốt và không nhất thiết phải phức tạp như ác mộng

  • Hơi lạc đề, nhưng tôi rất mừng vì đây là lần đầu thấy một giao diện WebAssembly mà văn bản trông không bị mờ. Thật sự là lần đầu
    Nếu mở rộng ra các chương trình và một số hệ điều hành, chẳng hạn cả Windows, thì trong vài năm qua cách raster hóa văn bản này đã trở thành xu hướng chung và thiết lập mặc định, gây ra vấn đề trên diện rộng
    Đáng tiếc là người dùng thường không thể tắt anti-aliasing để có văn bản sắc nét, và hiếm khi có tùy chọn thì anti-aliasing vẫn được áp dụng cho các giao diện như menu

    • Văn bản sắc nét đó không ấn tượng lắm. Kiểu chữ không có đường cong và cũng không có smoothing. Ở bất kỳ độ phân giải nào, nó sẽ trông như chữ góc cạnh dạng khối
    • Tôi tò mò không biết có mối liên hệ gì giữa WebAssembly và raster hóa. Nghe thật sự thú vị
  • Tôi rất thích những dự án kiểu này. Tôi vẫn thích tính cấp thấp của C. Hiện giờ tôi dùng Rust và Elixir/Erlang nhiều, nhưng đôi khi vẫn nhớ sự đơn giản và rõ ràng của C
    Vì vậy tôi cũng dùng Zig khá nhiều; đó là một ngôn ngữ giữ lại nhiều triết lý của C nhưng cải tiến rất ổn

    • Erlang cũng là một ngôn ngữ khá đơn giản
  • Tôi thật sự đồng ý với nhận định của anh ấy về C. Đặc biệt là đoạn “cú pháp không che giấu các hành vi phức tạp. Nó đủ đơn giản để không phải tra cứu liên tục”; hơn nữa, ngay cả khi cần tra cứu điều gì đó về C thì cũng rất dễ và bổ ích
    Một ngôn ngữ đơn giản và lâu đời có những lợi thế riêng của nó

  • Nếu cấp phát riêng từng Shape bằng malloc rồi lưu các con trỏ đó vào một mảng động thì chắc chắn có thể tự làm mọi thứ khó hơn. Có ý nói rằng nếu dùng một ngôn ngữ như C# thì sẽ bị ép theo cấu trúc cấp phát như vậy, nhưng tôi thắc mắc điều gì ngăn cản việc dùng mảng cố định các struct trong C# như tác giả đã làm trong C

    • Không có gì ngăn cản cả. Trong C#, việc dùng mảng struct kiểu này cũng không phải là hiếm
  • Mong có ai đó tiếp tục phát triển dự án này. Chỉ cần tinh chỉnh thêm vài tháng, nó có thể trở thành một lựa chọn thay thế nghiêm túc cho Blender hoặc FreeCAD trong một số mục đích cụ thể, và đường cong học tập trông cũng thoải hơn nhiều

    • Cũng nên xem qua MagicaCSG, một phiên bản tinh vi hơn mà vẫn miễn phí: https://ephtracy.github.io/index.html?page=magicacsg#ss-caro...
    • Chỉnh sửa: À, chương trình này đã hỗ trợ xuất mesh bằng marching cubes. Xem video YouTube trên trang web là thấy. Tôi đã không biết điều đó
      Tuy nhiên, vì về cơ bản nó hoạt động bằng SDF, nên cả trải nghiệm mô hình hóa lẫn dữ liệu được lưu trữ đều khác với mesh truyền thống dùng tam giác, đỉnh, v.v.
      Có thể chuyển SDF thành mesh bằng các phương pháp như marching cubes, nhưng dữ liệu như vậy nhiều khả năng rốt cuộc vẫn phải được dọn dẹp trong các ứng dụng kiểu Blender
      Nếu renderer cũng dựa trên SDF thì SDF rất tuyệt. Nhưng đa số thì không
      Xin lỗi nếu bạn đã biết rồi
    • Nếu thích SDF, Womp là một điểm khởi đầu khá tốt. Tinkercad cũng khá ổn cho CAD nhập môn
    • Cũng nên xem qua Dune3D và Salome-Platform
    • Có thể là lựa chọn thay thế cho Blender, nhưng đáng tiếc có lẽ không phải là lựa chọn thay thế cho công việc CAD