2 điểm bởi GN⁺ 2024-05-03 | 1 bình luận | Chia sẻ qua WhatsApp

Tạo trình tạo mô hình 3D bằng C trong một tuần

  • Tác giả tham gia sự kiện lập trình kéo dài một tuần có tên "Wheel Reinvention Jam" vào mùa thu năm ngoái
  • Mục tiêu là nhìn lại các hệ thống phần mềm hiện tại theo một góc nhìn mới
  • Đã tạo ra một trình tạo mô hình 3D tên là "ShapeUp"; việc xem trước video demo của ShapeUp trước khi đọc bài này sẽ giúp dễ hiểu hơn
  • ShapeUp có thể dùng thử trực tiếp trong trình duyệt

Lựa chọn ngôn ngữ: C

  • Tham gia Jam vì bực mình với độ chậm của trình biên dịch TypeScript
  • Nếu bắt đầu với bộ phân tích cú pháp TypeScript của esbuild hoặc Bun, dự án cài đặt một tập con nhanh của TypeScript dường như có thể làm được
  • Nhưng chuyển sang hướng dự án 3D vì nghĩ rằng việc so sánh tốc độ chạy lệnh terminal sẽ không thành một demo thú vị
  • Nhờ kỹ thuật Ray Marching Signed Distance Fields (SDF), việc tạo một dự án 3D từ đầu trong một tuần có vẻ khả thi
  • Cảnh dựng bằng SDF có thể được xây nhanh hơn nhiều so với renderer dựa trên tam giác tương đương
  • Tôi đã từng viết shader SDF trước đó nhưng chỉ ở mức rất cơ bản, và việc chỉnh sửa mã để tạo hình trông không tự nhiên
  • Muốn chỉnh sửa hình dạng bằng chuột, và tôi xem đây là cơ hội của Jam để biến điều đó thành hiện thực
  • Đặt tên dự án là ShapeUp

Lợi ích khi dùng C

  • C là ngôn ngữ rất đơn giản và sơ cấp, nên có người cho rằng sẽ tốn nhiều thời gian để bù đắp thiếu cấu trúc dữ liệu tích hợp và sửa lỗi con trỏ
  • Nhưng sự đơn giản của C lại trở thành điểm mạnh
    • Tốc độ biên dịch nhanh
    • Ngữ pháp không che giấu các phép toán phức tạp
    • Rất đơn giản nên không phải liên tục tra cú pháp
    • Dễ biên dịch sang native và WebAssembly
  • Những điểm yếu của C có thể né tránh bằng thói quen đã xây đắp qua 22 năm dùng ngôn ngữ này
  • ShapeUp gồm rất ít, chỉ là một file C đơn lẻ nên cực kỳ giản dị

Cấu trúc dữ liệu của ShapeUp

  • Mô hình được cấu thành từ mảng các struct Shapes
  • Các Shapes được lưu trong mảng cấp phát tĩnh
    • Không có nguy cơ cấp phát thất bại hoặc rò rỉ bộ nhớ
    • Giới hạn 100 Shape thực tế không phải là nút thắt
    • Nếu thiếu thời gian tối ưu renderer, tốc độ khung có thể đã giảm trước khi đạt tới 100
    • Nếu có thời gian, tôi đã chia mô hình thành các block nhỏ và raymarching trong từng block
  • Bộ nhớ động chỉ được gọi malloc đúng 3 nơi
    • Lưu (cấp phát bộ đệm đủ lớn để chứa toàn bộ tài liệu)
    • Xuất OBJ (cấp phát bộ đệm đủ lớn để chứa toàn bộ đỉnh)
    • Tạo shader GLSL (bộ đệm cho mã nguồn shader)
  • Trong mọi trường hợp đều có một free đơn lẻ ở cuối hàm
  • Ví dụ cho thấy quản lý bộ nhớ trong C có thể rất đơn giản
  • Các ngôn ngữ như C#, JavaScript, Python buộc phải cấp phát riêng cho từng Shape rồi lưu các con trỏ của chúng trong mảng động
  • C rất tốt vì kiểm soát được bố cục bộ nhớ

Giao diện người dùng

  • Được triển khai theo giao diện chế độ tức thời (immediate mode user interface, IMGUI)
  • Tôi thích UI kiểu IMGUI
    • Dễ debug
    • Dùng ngôn ngữ lập trình thực để đặt các phần tử (không giống CSS, constraints, SwiftUI)
  • Giống hầu hết IMGUI, dùng enum để theo dõi phần tử nào có focus và hành vi của chuột
  • Dự án này không cần mảng động hay hashmap; nếu có, tôi sẽ dùng thư viện kiểu stb_ds.h

Vấn đề với thư viện Raylib

  • Việc chọn C là đúng, nhưng raylib lại là điểm khó chịu
  • Các lựa chọn thiết kế kỳ quặc làm giảm trải nghiệm phát triển
    • Dùng int thay cho enum ở chỗ đáng ra nên là enum, khiến kiểm tra kiểu của trình biên dịch không hoạt động và hàm không tự mô tả
    • Không có kiểm tra tính hợp lệ của tham số cơ bản (một lựa chọn thiết kế)
    • Không chịu trách nhiệm cho phụ thuộc (không giải quyết issue của GLFW hay gửi patch)
  • Thư viện UI raygui chỉ là đồ chơi
    • Không hiển thị được số thực
    • Không xử lý routing sự kiện chuột cho phần tử chồng nhau hoặc bị clip
    • Không tạo được góc bo tròn
    • Không thể tạo kiểu giao diện đẹp
  • Cũng có bug
    • Bug ngăn thay đổi font
    • Hàm vẽ không chia sẻ đỉnh giữa các tam giác nên tạo ra khoảng hở theo pixel
  • Mỗi lần phát hiện lỗi đều báo cáo, nhưng hầu hết bị đóng với trạng thái won't fix, và thời gian điền bug report quá tốn nên tôi bỏ cuộc
  • Việc tạo sẵn cửa sổ OpenGL là điều tốt, nhưng phải trả giá khá cao cho sự tiện lợi đó
  • May mắn là vẫn có lối thoát: dùng trực tiếp hàm OpenGL hoặc tự cài các chức năng từ đầu
  • Tới đây sẽ chuyển sang dùng sokol

Quá trình phát triển trong một tuần

  • ShapeUp gồm bốn phần lớn cần hoàn thành trong 6 ngày
    1. Giao diện người dùng (công cụ 3D, phím tắt bàn phím, sidebar, game controller)
    2. Bộ tạo shader GLSL + renderer Ray Marching
    3. Chọn chuột dựa trên GPU
    4. Marching Cubes for export
  • Mỗi phần đều không quá khó, nhưng khó ở việc đặt đúng thứ tự ưu tiên và không bị lệch hướng
  • Những vấn đề khó hoặc tốn thời gian được giải quyết bằng cách thiết kế lại hoặc dùng giải pháp tạm thời hoạt động ở 90% trường hợp
  • Đôi khi chỉ cần hoãn một tính năng khoảng một ngày, tôi lại vô thức tìm ra cách giải quyết
  • Luôn giữ một trình tạo mô hình 3D có thể chạy được và cải thiện dần khi có thời gian
  • Tôi nghĩ như việc làm kim tự tháp: làm theo từng tầng, cho tới cuối có thể chưa hoàn thiện toàn bộ kim tự tháp, nhưng dừng ở tầng nào cũng tạo được một kim tự tháp hoàn chỉnh

Kết quả dự án

  • Sau một tuần, tôi có một chương trình 3D có thể tạo mô hình có ý nghĩa và xuất sang file .obj
  • Nó chạy đa nền tảng và có khả năng mở/lưu file
  • Dự án gồm 2.024 dòng mã C và 250 dòng GLSL
  • Gần 2.300 dòng đủ để diễn tả một trình tạo mô hình 3D tương đối hữu ích, điều này hơi khiến tôi ngạc nhiên
  • Nhận được lời mời trình diễn demo ShapeUp trong phần tổng kết Jam và tại hội nghị Handmade Seattle
  • Mọi người dường như khá ấn tượng với ShapeUp, nhưng tôi không nghĩ đây là một đột phá lớn vì đây vẫn là dự án tương đối đơn giản
  • Nếu có thứ gì đáng nói trong những gì tôi đã làm, thì đó là cảm giác chọn thứ cần làm, kiến thức để làm được nó, và kỷ luật để hoàn thành trong một tuần

Ý kiến của GN+

  • Đây là một dự án thú vị cho thấy rõ điểm mạnh của C ở sự đơn giản và tốc độ, nhưng do mức trừu tượng hóa thấp của C nên rất khó áp dụng nguyên trạng cho sản phẩm thương mại; để cài đặt toàn bộ tính năng của một công cụ mô hình hóa 3D hiện đại bằng C có vẻ cần nỗ lực khổng lồ
  • Việc hoàn thành một chương trình chạy được trong một tuần là ấn tượng. Nhưng về dài hạn, khi xét đến bảo trì và mở rộng tính năng, có thể lựa chọn C++ hoặc Rust sẽ hợp lý hơn
  • Kỹ thuật render bằng SDF nhanh và đơn giản nhưng có vẻ giới hạn về độ tự do và chất lượng mô hình hóa; công cụ thương mại thường dựa vào kỹ thuật bề mặt như SubD hoặc NURBS. Tuy nhiên, trong game hay demo nơi thời gian thực quan trọng, SDF rendering vẫn có giá trị ứng dụng cao
  • Đây là ví dụ rõ ràng cho thấy khó khăn khi chọn thư viện mã nguồn mở. Cần đánh giá cẩn thận tài liệu, chất lượng mã và mức độ hỗ trợ; tự triển khai cũng có thể là lựa chọn tốt
  • Cách làm việc với một chương trình chạy được trước, rồi cải thiện dần theo thời gian rất hữu dụng trong thực tế. Có vẻ quan trọng là ưu tiên hoàn thiện xong chức năng cốt lõi trước rồi mới tinh chỉnh chi tiết

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ề những hạn chế của Raylib
    • Hiện tại tôi đang phát triển một game theo kiểu tower defense bắt đầu với Raylib nhưng cũng gặp phải các giới hạn tương tự
    • Có các vấn đề như chuyển đổi toàn màn hình không nhất quán giữa các nền tảng, không thể liệt kê chế độ hiển thị, không thể chuyển đổi tính năng render ở runtime, không thể lưu shader đã biên dịch, v.v.
    • Raylib rất tốt cho việc làm prototype, nhưng nếu không chấp nhận các hạn chế nghiêm trọng thì khó có thể đi xa hơn
    • Dự án đã tiến triển quá xa nên giờ đã muộn để thay Raylib bằng SDL hoặc công cụ khác
  • Việc lưu hình dạng trong mảng tĩnh là một cách dễ thương vì không có rủi ro thất bại cấp phát hay rò rỉ bộ nhớ; thực tế, giới hạn 100 hình dạng không phải rào cản.
  • Hy vọng dự án này tiếp tục phát triển. Sau vài tháng, nó có thể trở thành một giải pháp thay thế nghiêm túc cho một số trường hợp sử dụng cụ thể của Blender/FreeCAD.
  • Tôi thực sự thích phần demo trực tiếp trong video. Chưa kể đến việc làm app, sẽ không thể làm được một video như vậy chỉ trong một tuần.
  • Bài viết thú vị khi bàn về nhiều quyết định như xử lý bộ nhớ. Với tư cách người đang học lại C khi bắt tay vào Crafting Interpreters Part 2, nó gợi lại cho tôi C làm tốt việc gì.
  • Cảm ơn vì nỗ lực đã giúp biến điều gì đó dường như mang tính nghịch lý thành hiện thực bằng 2024 dòng mã C :)
  • Có thứ gì đó cực kỳ mạnh mẽ khi chỉ cần dùng một công cụ quen thuộc để tạo ra điều thật tuyệt. Bài viết rất hay.
  • Tôi thật sự đồng tình với lập luận về C. Đặc biệt là câu: "cú pháp không che giấu những phép toán phức tạp. Nó đủ đơn giản nên không cần phải mãi phải tra cứu". Hơn nữa, nếu cần tìm hiểu về C thì việc đó rất dễ và bổ ích. Đây là lợi thế của một ngôn ngữ đơn giản, đã qua nhiều thời gian.
  • Đôi khi C chính là tất cả những gì chúng ta cần.
  • Tốc độ phát triển thật ấn tượng. Video giải thích cũng vô cùng thú vị khi xem!