Kết xuất văn bản không cần texture
(poniesandlight.co.uk)- Trong những tình huống cần in nhanh thông điệp gỡ lỗi bên trong render pass, việc chuẩn bị font atlas là một gánh nặng, nên cách vẽ văn bản chỉ bằng hằng số fragment shader sẽ rất hữu ích
- Glyph được biểu diễn bằng bitmap 8x16 pixel, và 96 ký tự ASCII có thể in được được lưu trong mảng
uvec4dài 1536 byte để shader tra cứu trực tiếp - Dữ liệu font có thể được tạo bằng cách lấy 96 glyph tiếp theo từ font terminal PSF1, sau khi bỏ qua phần header 4 byte và vùng glyph không in được 512 byte
- Việc render dùng
uint32_ttheo đơn vị 4 ký tự và một struct per-instance chứa vị trí cùng tỉ lệ, để vẽ toàn bộ văn bản bằng một instanced draw call duy nhất - Vì đây là kỹ thuật tập trung vào xuất gỡ lỗi đơn giản, các ràng buộc như đệm 4 ký tự, ký tự điền
\0,discard, và hiệu chỉnh endianness phải được xử lý phối hợp giữa ứng dụng và shader
Dùng hằng số shader thay cho font atlas
- Cách render văn bản thông thường là render các glyph có thể dùng vào font atlas, bind nó như một texture, rồi vẽ các tam giác tương ứng với từng glyph
imguivàstb_truetypecũng dùng cách này, nhưng với việc xuất nhanh thông điệp gỡ lỗi thì phần chuẩn bị có thể khá phiền phức- Một phương án thay thế là lưu dữ liệu tương ứng với font atlas dưới dạng hằng số số nguyên bên trong fragment shader
- Số nguyên có thể được dùng như bitmap, ánh xạ vị trí
xycủa fragment tới một vị trí bit cụ thể; nếu bit đó bật thì xuất màu tiền cảnh, nếu tắt thì xuất màu nền
Glyph bitmap 8x16 và bảng ASCII
- Một byte chỉ biểu diễn được pixel của một dòng, nên để tạo glyph dễ đọc hơn, mỗi glyph dùng 16 byte
- Mỗi glyph trở thành một canvas 8x16 pixel, và một
uvec4của GLSL chứa vừa đúng 16 byte cần thiết - Nếu lưu 96 glyph ASCII có thể in được thì tổng dữ liệu là 1536 byte
- Mảng
font_data[96]dùng chỉ số bằng giá trị ASCII trừ0x20- Áp dụng cho các glyph ASCII có thể in được bắt đầu từ
0x20SPACE - Để tiết kiệm chỗ, mã ví dụ chỉ hiển thị một phần các mục
- Áp dụng cho các glyph ASCII có thể in được bắt đầu từ
- Bảng bitmap đầy đủ có trong mã nguồn Island
Trích xuất bitmap từ font PSF1
- Cách mã hóa bitmap cần dùng gần như trùng khớp với font terminal định dạng PSF1
- Quy trình trích xuất dữ liệu từ font terminal PSF1 khá đơn giản
- Mở file font bằng trình soạn thảo hex như ImHex
- Bỏ qua header 4 byte
- Bỏ qua phần glyph không in được dài 512 byte
- Xuất 96 glyph tiếp theo, tức 1536 byte, bằng “Copy as → C Array”
- Mảng char đã trích xuất có thể được chỉnh thành mảng
uintrồi nhóm lại theo đơn vịuvec4 - Nếu ghép trực tiếp raw char thành
uintthì endianness sẽ bị đảo, nên khi lấy mẫu cần hiệu chỉnh lại - Dữ liệu gốc của font pixel được dùng lấy từ font pixel miễn phí Tamsyn của Scott Fial
Cấu hình một instanced draw call duy nhất
- Việc render văn bản được xử lý bằng một instanced draw call
- Draw call dùng hai luồng attribute
- Luồng per-draw chỉ chứa thông tin cần thiết để vẽ một quad thông thường
- Luồng per-instance chứa độ lệch vị trí trên màn hình và đoạn văn bản cần xuất
- Phần độ lệch vị trí dùng hai số thực x, y, và khoảng trống float còn lại có thể dùng để chứa giá trị scale của font
- Trong Vulkan, mọi component của vertex output binding phải có cùng đặc tính nội suy, nên khó trộn gọn gàng
vec3vàuinttrong cùng một binding - Văn bản được đóng gói vào
uint32_ttheo đơn vị 4 ký tự- Kiểu dữ liệu attribute vertex cơ bản thường có đơn vị tối thiểu là 32 bit, nên có thể chứa 4 ký tự trong một lần
- Độ dài thông điệp phải chia hết cho 4
- Phần thiếu sẽ được điền bằng ký tự
\0
- Dữ liệu per-instance được biểu diễn bằng struct
word_datapos_and_scale[3]: vị tríxyvà scaleword: bốn ký tự cần in
- Ứng dụng chia thông điệp thành các khối 4 ký tự, chuyển từng khối thành
uint32_t, rồi tích lũy chúng cùng độ lệch vị trí vào mảngword_data - Khi render, mảng này được bind làm per-instance binding cho pipeline vẽ debug text, và vẽ số instance tương ứng với số quad
Truyền vị trí và ký tự trong Vertex Shader
- Vertex shader xuất
gl_Position,wordcần render, và giá trị tương ứng với tọa độ texture gl_Positiondùng dữ liệupos_and_scaleper-instance để đặt các đỉnh tam giác lên màn hình trong hệ tọa độ NDC- word cần render được truyền nguyên từ attribute đầu vào kiểu
uintsang fragment shader- Dùng qualifier
flatđể tránh bị nội suy
- Dùng qualifier
- Tọa độ texture được tổng hợp từ
gl_VertexIndex12 >> gl_VertexIndex & 1tạo ra chuỗi0, 0, 1, 19 >> gl_VertexIndex & 1tạo ra chuỗi1, 0, 0, 1- Kết hợp này tạo ra tọa độ uv
(0,1), (0,0), (1,0), (1,1)mà không cần phân nhánh
- Vertex shader cũng nhận màu tiền cảnh và màu nền từ dữ liệu per-instance rồi truyền sang fragment shader
Lấy mẫu glyph trong Fragment Shader
- Để fragment shader render được văn bản, cần ba loại thông tin
- Tọa độ uv của fragment sau nội suy
- Dữ liệu ký tự cần xuất
in_word - Mảng bitmap glyph
font_data
- Tọa độ uv là dải float chuẩn hóa từ
vec2(0.f,0.f)tớivec2(1.f,1.f), còn tọa độ pixel glyph là từuvec2(0,0)tớiuvec2(7,15) - Toàn bộ word 4 ký tự được xem như một vùng rộng 32 pixel và cao 16 pixel
- Lượng tử hóa thành tọa độ pixel của word bằng cách
floortrênuv.xy * vec2(8 * WORD_LEN, 16) - Giới hạn phạm vi tọa độ trong
uvec2(0..31, 0..15) - Dùng
word_pixel_coord.x / 8để xác định đang nằm trong vùng của ký tự nào trong bốn ký tự - Dùng
word_pixel_coord.x % 8để lấy tọa độ x bên trong glyph
- Lượng tử hóa thành tọa độ pixel của word bằng cách
- Mã ký tự được chuyển thành chỉ số của
font_data- Vì glyph đầu tiên là
0x20SPACE, nên dùngprintable_character - 0x20làm offset - Lấy bitmap glyph dạng
uvec4tương ứng bằng offset đó
- Vì glyph đầu tiên là
- Tọa độ y dùng
glyph_pixel_coord.y / 4để chọnuintcụ thể bên tronguvec4uintnày chứa dữ liệu pixel của 4 dòng- Do char trích từ ImHex được nối trực tiếp để tạo
uint, thứ tự dòng bị đảo ngược - Cần hiệu chỉnh bằng cách đánh chỉ số từ cuối theo dạng
(8*(3-(glyph_pixel_coord.y)%4))
- Tọa độ x dùng
7-glyph_pixel_coord.xđể chọn bit- Bit có trọng số lớn nhất của byte được lưu ở chỉ số cao nhất, nên muốn ánh xạ từ trái sang phải phải đánh chỉ số ngược
- Cuối cùng, dùng giá trị
current_pixelđể áp dụngmix(background_colour, foreground_colour, current_pixel)và quyết định màu
Xử lý chuỗi ngắn và ký tự điền
- Nếu độ dài chuỗi không chia hết cho 4, ứng dụng sẽ điền phần thiếu bằng ký tự
\0 - Fragment shader kiểm tra xem ký tự cần xuất có phải
\0hay không - Khi gặp ký tự điền
\0, nó không vẽ cả nền mà thực hiệndiscard - Cách xử lý này giữ nguyên ràng buộc đóng gói 4 ký tự mà vẫn cho phép xuất chuỗi ngắn
Cách dùng và vị trí mã
- Trong dự án Island, có thể gọi
le::DebugPrintđể xuất văn bản gỡ lỗi lên màn hình - Toàn bộ mã fragment shader có thể xem trên github
- Ví dụ mã truyền dữ liệu chuỗi để hiển thị thông điệp dạng
"That's all, %s"trên màn hình
1 bình luận
Ý kiến trên Hacker News
Nếu muốn tự thử, chỉ cần lần theo phần số học. Việc triển khai từ đầu trên ShaderToy cực kỳ dễ, và nếu bạn thích những thứ kiểu này thì cũng là một trò vui sáng thứ Bảy
Tự làm từ đầu thì thú vị, nhưng nếu cần gợi ý khởi đầu thì đây là ví dụ tôi vừa tạo: https://www.shadertoy.com/view/Mc3cW2
Cũng có rất nhiều mẹo xử lý chữ thông minh do người khác làm, như ví dụ Matrix dưới 300 ký tự https://www.shadertoy.com/view/llXSzj hay hiệu ứng hiển thị CRT xanh lá https://www.shadertoy.com/view/XtfSD8
vec2(30, -30)thành 300 thì sẽ thấy artifactTôi tò mò không biết có mẹo nào để xử lý đúng không. Trong trường hợp của tôi, multisampling texture bên trong fragment shader là hiệu quả nhất, nhưng vẫn chưa tốt bằng mức hiện đại
Cách đây không lâu tôi thử làm một ứng dụng trông như phông chữ console native, mà chỉ để đạt khoảng 90% thôi cũng phải loay hoay hơn 2 giờ
Kỳ quặc, hacky và vui. Thật ra gần như mọi kỹ thuật render 3D đều như vậy, nhưng kết quả không hẳn là đẹp nếu mục tiêu không phải tái hiện các BBS ngày xưa
Có thể cải thiện bằng cách thêm nhiều bit hơn, nhưng rất lâu trước khi nó trông đẹp, bạn sẽ tìm ra một cách dễ hơn để đặt tất cả các bit. Cuối cùng sẽ quay về điểm xuất phát, vì hầu như không có giải pháp nào hiệu quả hơn việc tạo các pixel đen trắng trong chương trình vẽ rồi lưu thành texture
Nếu tò mò về cách phổ biến hơn mà các engine render 3D hiện đại dùng để vẽ chữ, hãy tìm SDF text và các kỹ thuật liên quan như MSDF. Đó là cách dùng texture atlas truyền thống ở bước tiền xử lý để tạo atlas trường khoảng cách có dấu
Bài đó nói về phần cứng, nhưng phần mềm cũng có luân hồi
Trước đây tôi từng làm một phiên bản rất cơ bản của ý tưởng này: https://www.shadertoy.com/view/sdXBDs
Không có ý nói đây không phải là một thủ thuật hay; thực ra nó là một thủ thuật hay
Cũng có lựa chọn render văn bản dưới dạng mesh. TextMeshPro còn tiến thêm một bước, dùng trường khoảng cách có dấu để xử lý tỉ lệ tùy ý
https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...
Ví dụ: https://sluglibrary.com
Mesh và SDF đơn giản hơn nhiều ở phía GPU, nhưng nếu phóng quá lớn thì độ chính xác có thể giảm, còn nếu thu mesh quá nhỏ thì có thể sinh aliasing
Rất tuyệt. Sẽ thú vị nếu có so sánh hiệu năng với cách dùng texture “truyền thống”
Với những tác vụ đơn giản kiểu này trên GPU ngày nay, câu trả lời cho câu hỏi “hiệu năng thế nào?” dường như thường là “chạy được”
Sebastian Lague đã làm một video hay về nhiều kỹ thuật render phông chữ
https://youtu.be/SO83KQuuZvg
Tôi từng thử một kỹ thuật tương tự, trong đó nhét toàn bộ dữ liệu phông chữ vào mã nguồn fragment shader. Như vậy có thể dùng
snprintfđể in trực tiếp vào bộ đệm GPU được ánh xạ trên CPU. Tôi biết đây là cách làm nguy hiểmThay vì vẽ từng ký tự bằng vertex shader, tôi chỉ vẽ một tam giác toàn màn hình và dùng
gl_FragCoordthay cho tọa độ UV. Đây không phải cách hiệu quả nhất, nhưng là tính năng debug và trong thực tế đủ nhanhDù tên tệp là vậy, tôi dùng phông chữ từ IBM PC ROM chứ không phải NES. “NES font” và các phông chữ 8x8 pixel khác có thể tìm thấy trên web
https://github.com/rikusalminen/triangles/blob/nesfont/shade...
Điều thú vị là game này lưu dữ liệu câu hỏi và câu trả lời trên băng 8-track
Hay đấy. Không thường thấy một thuật toán render văn bản mà tôi chưa tự thử qua. Ở startup của mình tôi đã triển khai nhiều cách khác nhau, nhưng vì tôi cần độc lập độ phân giải và khử răng cưa nên cách này có lẽ đã không giúp được gì
Nó cũng có thể không tổng quát hóa được cho mọi tệp phông chữ dùng đường cong Bézier. Chuyển đường cong thành pixel có thể khó, đặc biệt khi glyph tự giao cắt với chính nó. Nhìn chung, render văn bản tiêu chuẩn có cảm giác là bài toán đã được giải, còn các trường hợp sử dụng phi tiêu chuẩn thì rất khắc nghiệt để thử nghiệm
Về mặt ý tưởng, cách này trông giống phương pháp của Will Dobbie mà tôi thích, chỉ là đơn giản hơn nhiều. Cả hai đều lấy dữ liệu phông chữ thô và dùng trực tiếp trong shader. Khác biệt là cách này lưu dữ liệu pixel trong mảng, còn Will lưu dữ liệu đường dẫn SVG dưới dạng “texture vector”
Nếu tò mò, Will có một demo rất ấn tượng: https://wdobbie.com/warandpeace/
Trước đây tôi từng nghĩ đến việc thử làm kiểu này, nhưng hiểu rằng GPU đặc biệt hiệu quả với render texture và tương đối chậm với thao tác bit. Dù tiết kiệm được chút bộ nhớ ở đây, tôi tự hỏi liệu thực tế nó có nhanh hơn dùng atlas không
Có lẽ nếu bit-pack vào một texture thông thường rồi để fragment shader giải mã thì có thể được lợi cả hai phía
Tôi gần như không biết gì về đồ họa máy tính hiện đại nên hỏi thuần túy thôi: chi phí upload một texture nhỏ lên GPU có lớn đến vậy không? Tôi tự hỏi liệu có thể render toàn bộ chuỗi thành texture 2D rồi hiển thị texture đó trên hai tam giác không
Ngược lại, bạn phải viết mã tạo font atlas, hoặc tìm và tải một atlas có sẵn, mà như vậy lại cần mã tải. Hoặc phải vẽ toàn bộ thông điệp lên texture và cache kết quả đó cho đến khi thông điệp thay đổi
Rồi còn phải quản lý tài nguyên và binding nữa, trong khi cách này không cần tài nguyên. Nói cách khác, đây không phải giải pháp văn bản thông thường mà là kỹ thuật để hiển thị văn bản debug trên màn hình
Nhân tiện, hầu hết trình duyệt và hệ điều hành hoạt động bằng cách vẽ chữ lên texture. Chúng vẽ phông chữ động vào texture atlas, rồi dùng các glyph trong atlas đó để tạo thêm texture cho một phần cửa sổ ứng dụng. Nếu bật hiển thị ranh giới texture trong trình duyệt, bạn có thể thấy tất cả texture; Rendering->Layer borders sẽ viền từng texture bằng màu xanh lơ
Nếu CPU tương đối chậm phải render nhiều hộp văn bản texture độc lập, chi phí sẽ nhanh chóng cộng dồn và ăn vào ngân sách
Vẽ bằng glyph atlas vẫn tốt hơn nhiều về mặt sử dụng tài nguyên. Các pipeline render văn bản hiện đại thường dùng SDF hoặc đường cong Bézier được mã hóa để tăng độ dễ đọc của glyph khi phóng to/thu nhỏ, và đây cũng là một cách tốt để tiết kiệm bộ nhớ hơn
Từ góc nhìn upload, cuối cùng vẫn có X byte glyph và bằng cách này hay cách khác chúng phải vào bộ nhớ GPU. Dù là texture, dữ liệu uniform hay hằng shader thì về hiệu năng cũng không khác biệt lớn. Ngược lại, nếu đưa vào dạng hằng shader như bài gốc, mọi khai báo hằng đều phải được trình biên dịch shader xử lý, nên chi phí phía CPU có thể còn lớn hơn
Ở phía GPU, điều quan trọng là khi đọc dữ liệu glyph thì chạm vào tầng bộ nhớ nào. Texture fetch trên hầu hết GPU dùng cache L1 chuyên dụng, và có lẽ lớn hơn cache L1 thông thường. Thứ tự dữ liệu cũng quan trọng. Texture thường được lưu theo một biến thể của thứ tự Morton để tránh cache miss khi shading các khối pixel. Nếu là renderer văn bản dựa trên atlas dùng thực tế, có lẽ nên dùng texture
Sửa: tôi đã đọc nhầm câu hỏi. Nếu đang so sánh việc vẽ từng glyph riêng trên GPU với việc vẽ cả khối văn bản trên CPU, thì đây là đánh đổi giữa tốc độ và không gian. Câu trả lời phụ thuộc vào việc bạn muốn dùng bao nhiêu bộ nhớ cho văn bản, văn bản có thay đổi không, có cần hiệu ứng theo từng ký tự không, v.v.
Nói thêm, dù gọi là “không texture”, cách này cũng vẫn là texture. Chỉ là texture được lưu ở định dạng khác và vị trí khác. Render phông chữ thật sự không dùng texture là đánh giá các đường cong vector ngay tại chỗ
Nói là sẽ không lưu bitmap trong shader, nhưng rồi lại giải thích chính xác cách lưu bitmap trong shader, nên khá dễ gây nhầm lẫn
Tóm lại là đã nhúng phông chữ bitmap vào shader
Có thể ví như sự khác nhau giữa lưu dữ liệu trong một tệp riêng cần đọc lúc runtime và đưa dữ liệu trực tiếp vào mã nguồn