การเรนเดอร์ข้อความคมชัดบน GPU
(osor.io)- วิธี Multi-Channel SDF แบบเดิมยังมีข้อจำกัดกับเส้นบาง ๆ, แอตลาสขนาดใหญ่, การย่อ·ขยาย และการเคลื่อนที่อย่างนุ่มนวล จึงนำมาสร้างใหม่เป็น การราสเตอร์ไรซ์ที่รันไทม์บน GPU
- ไปป์ไลน์ใหม่ส่งเฉพาะ ข้อมูลเส้นโค้ง Bézier ของ glyph ที่มองเห็นอยู่ในปัจจุบันไปยัง GPU แล้ววาดลงในแอตลาสที่รันไทม์ ก่อนจะสุ่มตัวอย่างบนหน้าจอ
- หาก glyph ยังคงอยู่บนหน้าจอ จะคงแคชแอตลาสไว้พร้อมสะสม sample เพื่อเพิ่มคุณภาพ โดยเริ่มจาก 8 samples-per-pixel ในเฟรมแรก และปรับละเอียดได้สูงสุดถึง 512 samples
- คีย์ของแคชประกอบด้วย font, glyph_index, ขนาดพิกเซล และ ออฟเซ็ตซับพิกเซล โดยใช้ fixed-point 8 บิตเพื่อพับตำแหน่งและขนาดที่ใกล้เคียงกันให้เป็นค่าเดียวกัน เพื่อเพิ่มการนำกลับมาใช้ซ้ำ
- หากจำลองโครงสร้างซับพิกเซล RGB ของแต่ละมอนิเตอร์เป็นพื้นที่ sample จะช่วยลดสีเหลื่อมได้แม้กับเลย์เอาต์ที่ไม่เป็นมาตรฐานอย่าง OLED G9 และเดโมข้อความเต็มหน้าจอ 4K แสดงค่าใช้จ่ายพีกประมาณ 0.1ms บน Radeon 9070
เหตุผลที่เลือกการราสเตอร์ไรซ์เวกเตอร์แบบรันไทม์แทน SDF
- การใช้งานเดิมใช้ Multi-Channel Signed Distance Fields และโดยรวมทำงานได้ดี แต่ในการใช้งานจริงยังมีข้อจำกัดอยู่หลายอย่าง
- คุณภาพ: ในฟอนต์ที่มีเส้นบางหรือรายละเอียดมาก คุณลักษณะบางอย่างหายไปหรือเกิดอาร์ติแฟกต์ และบางกรณีต้องใช้ SDF ที่มีความละเอียดสูงกว่า
- ขนาดแอตลาส: SDF ถูกสร้างแบบออฟไลน์แล้วเก็บในแอตลาส ทำให้ฟอนต์ญี่ปุ่น·จีนที่มี glyph จำนวนมากอบรวมไว้ในแอตลาสเดียวได้ยาก
- ความยืดหยุ่น: นำแนวคิดใหม่ ๆ อย่างปัญหาการย่อ·ขยายหรือ subpixel anti-aliasing มาใช้ได้ลำบาก และไม่เหมาะกับภาพเวกเตอร์ที่สร้าง·แก้ไขได้ตอนรันไทม์
- ความเรียบง่าย: ขั้นตอนแปลงเส้นโค้ง glyph ต้นฉบับเป็นเท็กซ์เจอร์กลางเพิ่มความซับซ้อนให้ระบบ
- แม้แต่ฟอนต์ละตินอย่าง Miama ก็ต้องใช้แอตลาส
4096×1152ที่ใช้พื้นที่64×64ต่อ glyph และหากใช้หลายฟอนต์ตอนรันไทม์ ต้นทุนหน่วยความจำกับแบนด์วิดท์สตรีมมิงจะสูงขึ้น - เป้าหมายของแนวทางใหม่นี้คือใช้ เส้นโค้ง Bézier ที่ผู้ออกแบบ glyph สร้างขึ้นโดยตรงมากขึ้น เพื่อลดขั้นตอนการแปลงจากข้อมูลต้นฉบับไปสู่พิกเซลสุดท้าย
ไปป์ไลน์โดยรวม
- แนวคิดหลักคือ แทนที่จะอบ glyph ทั้งหมดไว้ล่วงหน้าแบบออฟไลน์ ให้ส่งเฉพาะเส้นโค้งของ glyph ที่มองเห็นอยู่ในปัจจุบัน ไปยัง GPU แล้วราสเตอร์ไรซ์ในช่วงเวลาที่จำเป็น
- ลำดับการประมวลผลเรียบง่าย
- โหลดข้อมูลเส้นโค้ง glyph จากฟอนต์
- GPU ราสเตอร์ไรซ์ glyph ลงในแอตลาสตอนรันไทม์
- เมื่อแสดงผลบนหน้าจอ ให้ sample จากแอตลาสนั้น
- หาก glyph เดิมยังถูกใช้ต่อในเฟรมถัด ๆ ไป จะคงไว้ในแอตลาสและสะสม sample เพื่อปรับละเอียดจนเป็น subpixel anti-aliasing คุณภาพสูง
- เนื่องจากเรนเดอร์ representation แบบเวกเตอร์โดยตรง จึงรับมือกับการเปลี่ยนแปลงความละเอียดได้ง่าย และสามารถคำนวณได้ไม่เพียง coverage ทั้งพิกเซล แต่รวมถึง coverage ขององค์ประกอบซับพิกเซลแต่ละตัวด้วย
การประมวลผลเส้นโค้ง glyph
- ในการโหลดฟอนต์ ใช้ FreeType เป็นชั้นกลางของเครื่องมือออฟไลน์เพื่ออ่านฟอร์แมตที่รองรับ จากนั้นไล่ผ่านเส้นโค้งของแต่ละ glyph แล้วบันทึกเป็นฟอร์แมต asset ของตนเอง
- เส้นโค้ง glyph อาจประกอบด้วย เส้นตรง, quadratic Bézier, และ cubic Bézier และเพื่อให้ shader เรียบง่าย จะถูกแปลงทั้งหมดเป็น quadratic Bézier
- เส้นตรงถูกทำเป็น quadratic Bézier โดยเพิ่มจุดควบคุมหนึ่งจุดที่กึ่งกลางของสองจุด
- cubic Bézier ถูกแบ่งเป็น quadratic Bézier สองเส้น ซึ่งเป็นการแปลงลดดีกรีที่มีการสูญเสียข้อมูล
- วิธีแบ่ง cubic Bézier เป็น quadratic Bézier สองเส้นทำงานได้ดีกับฟอนต์ส่วนใหญ่ที่ลอง แต่ก็สามารถใช้วิธีที่ซับซ้อนขึ้นเพื่อลดความคลาดเคลื่อนให้มากกว่าเดิมได้
- หากต้องการการแปลงที่คุณภาพสูงกว่า สามารถใช้เครื่องมือออฟไลน์แปลงเป็นฟอร์แมตที่มีเฉพาะ quadratic Bézier อย่าง TrueType
.ttfเพื่อหลีกเลี่ยงการแปลงนี้เองได้ - สามารถเปรียบเทียบรูปทรงระหว่าง cubic Bézier อินพุตกับ quadratic Bézier สองเส้นที่เป็นผลลัพธ์ได้ใน กราฟ Desmos
การคำนวณ coverage และการปรับแต่งการเข้าถึงเส้นโค้ง
- coverage คำนวณแบบยิงรังสีแนวนอนระดับพิกเซลจากซ้ายไปขวา ตรวจจุดตัดกับเส้นโค้ง แล้วสะสม winding number
- พื้นฐานทางคณิตศาสตร์และการใช้งานอ้างอิงได้จาก GPU Font Rendering ของ GreenLightning และ วิดีโอ Rendering Text ของ Sebastian Lague
- ความไม่แม่นยำของการคำนวณจุดตัดอาจเกิดขึ้นใน sample ที่ความสูงบางระดับ แต่เมื่อสะสม sample หลายร้อยตัว ข้อผิดพลาดหนึ่งหรือสองตัวจะแทบมองไม่เห็นหลังเฉลี่ย
- เมื่อสะสมสูงสุด 512 samples หาก sample หนึ่งผิด จะได้
1/512=0.00195แทน0หรือ511/512=0.99804แทน1 - อาจกำหนด threshold เพื่อ clamp เมื่อ coverage ใกล้ค่าปลายสุดมาก ๆ ได้
- เมื่อสะสมสูงสุด 512 samples หาก sample หนึ่งผิด จะได้
- การกระจายตำแหน่ง sample ใช้ $R_2$ sequence ของ Martin Roberts และสามารถดูการกระจายตามเวลาได้จาก ตัวอย่าง Shadertoy
- เพื่อลดต้นทุนการเข้าถึงเส้นโค้ง จะแบ่ง glyph เป็น แถบแนวนอน หลายแถบ และเก็บเฉพาะเส้นโค้งที่แตะแต่ละแถบไว้เป็น bitset
- เนื่องจากติดตามเฉพาะรังสีแนวนอน จึงลดชุดเส้นโค้งที่ texel แต่ละตัวต้องตรวจได้มาก
- หากให้ระดับ wave เข้าถึงช่วงแถบเดียวกัน จะทำให้การวนและอ่านเส้นโค้งเป็น scalar ได้
- ตอนราสเตอร์ไรซ์ลงแอตลาสใน compute shader จะจัดแพ็กเธรดแบบ row-major ตามแนวนอน เพื่อลดช่วงแถบที่ wave แตะให้เหลือน้อยที่สุด
การแพ็กแอตลาสและคีย์แคช
- ตอนแรกเคยราสเตอร์ไรซ์ลงหน้าจอโดยตรง แต่ต้นทุนการคำนวณ anti-aliasing คุณภาพสูงทุกเฟรมนั้นสูง
- ข้อความส่วนใหญ่คงอยู่หลายเฟรมด้วยขนาดและตำแหน่งเดิม และ glyph เดียวกันมักซ้ำด้วยขนาดเดียวกัน จึงเหมาะกับ แอตลาสและการสะสมตามเวลา
- หากแอตลาสไม่มี glyph ที่ต้องการ จะจัดสรรพื้นที่แล้วเริ่มราสเตอร์ไรซ์ และหากมีอยู่แล้วก็ใช้ผลลัพธ์เดิมทันที
- ระหว่างเฟรมจะตรวจ glyph ในแอตลาสเพื่อตัดสินใจว่าจะคงไว้, sample เพิ่ม, หรือปล่อยพื้นที่เพราะไม่ได้ใช้งาน
- คีย์แอตลาสประกอบด้วยองค์ประกอบต่อไปนี้
fontglyph_indexquantized_size_in_pixels_x,quantized_size_in_pixels_yquantized_subpixel_offset_x,quantized_subpixel_offset_y
- ออฟเซ็ตซับพิกเซล ตรงกับ
frac(pixel_position)และจำเป็นต่อการสร้างผล anti-aliasing ที่ตรงกับตำแหน่งเมื่อ glyph ไม่พอดีกับกริดพิกเซลหรือเลื่อนอย่างนุ่มนวล - หากใช้ค่าทศนิยมเป็นคีย์ตรง ๆ ค่าที่ควรเท่ากันทางคณิตศาสตร์ก็อาจต่างกันในระดับบิต จึงพับตำแหน่งและขนาดที่ใกล้เคียงกันให้เป็นค่าเดียวกันด้วย fixed-point ส่วนทศนิยม 8 บิต
- ใน text editor ที่มีข้อความนิ่งจำนวนมาก หากใช้ฟอนต์ monospace ก็สามารถจัดระยะห่างตัวอักษรและตำแหน่งบรรทัดให้ตรงขอบพิกเซล เพื่อเพิ่มอัตรา hit ของแคชแอตลาสสำหรับ glyph เดียวกันได้
การจัดวางแอตลาสตาม Z-Order
- การจัดวาง glyph ตอนรันไทม์ใช้ Z-Order Packing และ bitset ของเซลล์ว่าง
- Z-Order ที่อิง Morton code ทำให้จัดการเซลล์ 2D เหมือนอาร์เรย์ 1D ยาว ๆ ได้ และเมื่อจัดสรรเซลล์ต่อเนื่องจำนวนเท่ากับกำลังของ 2 ก็จะกลายเป็นพื้นที่สี่เหลี่ยมจัตุรัสในแอตลาส 2D
- เซลล์พื้นฐานคือ texel ขนาด
16×16และขนาด glyph จะถูกปัดขึ้นเป็นกำลังของ 2 ถัดไป- เช่น glyph ขนาด
25×29จะถูกจัดสรรเป็นชังก์32×32 - ในกรณีนี้ต้องใช้เซลล์
16×16จำนวน 4 เซลล์ จึงค้นหาและใช้ 4 บิตต่อเนื่องที่จัดแนวแล้ว
- เช่น glyph ขนาด
- glyph ยาวและบางของอักษรละตินมักเป็นแนวตั้ง ดังนั้นหากใช้ Z-Order แบบทรานสโพส glyph อย่าง
l,j,i,1จะใช้พื้นที่เพียงครึ่งเดียวได้ - ในทางกลับกัน สำหรับอักษรอาหรับที่ glyph ยาวและบางเป็นแนวนอนมากกว่า Z-Order มาตรฐานจะเหมาะสมกว่า
วิธีเพิ่มคุณภาพด้วยการสะสมตามเวลา
- หาก glyph ยังคงอยู่ในแอตลาส ก็สามารถเติม sample เพิ่มทีละน้อยในแต่ละเฟรมเพื่อปรับปรุงผลลัพธ์ได้
- ตารางพื้นฐานคือ ในเฟรมแรกที่ glyph ปรากฏให้ 8 samples-per-pixel, เฟรมถัดไป 4, เฟรมถัดไป 2 จากนั้นเพิ่มเฟรมละ 1 sample จนสะสมครบ 512
- เหตุผลที่ตั้งคุณภาพเฟรมแรกไว้สูง คือ glyph ที่เคลื่อนที่อย่างนุ่มนวลหรือเปลี่ยนขนาดจะเหมือนถูกเริ่มใหม่ทุกเฟรม
- คุณภาพและประสิทธิภาพสามารถปรับได้หลายวิธี
- จำนวน sample·รังสีที่จะเพิ่มในแต่ละเฟรม
- จะเพิ่ม sample ในช่วงไม่กี่เฟรมแรกของ glyph หรือไม่
- เพดานจำนวน sample รวมต่อเฟรม
- time-slicing ที่อัปเดต glyph เดิมทุก ๆ หลายเฟรมแทนที่จะอัปเดตทุกเฟรม
- วิธีจำกัดต้นทุนการตรวจจุดตัดตามจำนวนเส้นโค้งของ glyph
- ใน implementation นี้ ประสิทธิภาพไม่ใช่ปัญหาใหญ่ และข้อความเต็มหน้าจอในช่วงนำเสนอมีค่าใช้จ่ายพีกประมาณ 0.1ms ที่ 4K บน Radeon 9070 เมื่อ glyph ถึงจำนวน sample สูงสุดแล้ว ต้นทุนจะลดลงเข้าใกล้ 0 อย่างรวดเร็ว
Subpixel anti-aliasing และสีเหลื่อม
- Subpixel anti-aliasing เรนเดอร์โดยมององค์ประกอบสีแดง·เขียว·น้ำเงินภายในพิกเซลของมอนิเตอร์เป็นพื้นที่ sample แยกกัน
- ในโครงสร้างแถบแนวตั้ง RGB ของ LCD แบบดั้งเดิม ความละเอียดแนวนอนจะเพิ่มขึ้นโดยพฤตินัย 3 เท่า ทำให้ที่ 4K สามารถมอง
3840×2160เหมือน3840×6480ได้ - ปัญหาคือมอนิเตอร์อย่าง OLED G9 มี โครงสร้างซับพิกเซลที่ไม่เป็นมาตรฐาน ซึ่งต่างจากแถบแนวตั้ง RGB มาตรฐาน
- หากสมมติเป็นโครงสร้างสี่เหลี่ยมแนวตั้ง RGB พื้นฐาน จะเห็นสีเหลื่อมเขียวด้านบนและแมเจนตาด้านล่าง
- หากตั้งตำแหน่ง sample ให้ตรงกับโครงสร้างซับพิกเซลจริงของมอนิเตอร์ สีเหลื่อมแทบไม่มีและได้ผลลัพธ์ที่นุ่มนวล
- Subpixel Zoo แสดงตัวอย่างโครงสร้างซับพิกเซลหลากหลายแบบ และ โครงสร้าง red-white-blue-green ของ LG WOLED เป็นตัวอย่างที่ต่างจากลำดับมาตรฐาน
- การมององค์ประกอบซับพิกเซลให้ใหญ่กว่าขนาดทางกายภาพจริงและตั้งให้ ซ้อนทับกัน ให้ผลลัพธ์แม่นยำกว่า
- แสงของซับพิกเซลผสมกันตามธรรมชาติและกระจายเล็กน้อย จึงทำงานเหมือนพื้นที่ sample ใหญ่กว่าซับพิกเซลทางกายภาพ
- พื้นที่ sample ควรกระจายออกไปนอกพิกเซลด้วย และผสมกับแสงซับพิกเซลของพิกเซลข้างเคียงด้วย
- Easy Scalable Text Rendering ของ Evan Wallace มองว่าหลัง subpixel anti-aliasing ต้องมีการเบลอแนวนอน ซึ่งโดยผลลัพธ์แล้วแทบเหมือนกับการมององค์ประกอบซับพิกเซลให้ใหญ่ขึ้นและซ้อนทับกัน
ซอฟต์แวร์ควรรู้ข้อมูลซับพิกเซลของจอแสดงผลได้
- หากเข้าถึงโครงสร้างซับพิกเซลแบบใดก็ได้ของมอนิเตอร์ จะช่วยปรับปรุงคุณภาพของ subpixel anti-aliasing และการเรนเดอร์ข้อความโดยทั่วไปได้
- หากข้อมูลนี้ถูกส่งผ่านโปรโตคอลจอแสดงผลทั่วไป แม้แต่มอนิเตอร์ที่มีเลย์เอาต์มาตรฐานก็สามารถเรนเดอร์ได้ละเอียดขึ้นตามฮาร์ดแวร์แต่ละรุ่น
- ผู้ผลิตจอแสดงผลจะไม่จำเป็นต้องหลีกเลี่ยงการทดลองโครงสร้างซับพิกเซลที่ดีกว่าเพราะปัญหาการเรนเดอร์ข้อความ
- Samsung เปลี่ยนโครงสร้างซับพิกเซลจาก G8 ไปเป็น G9 ใน QD-OLED เพื่อลดปัญหาเหล่านี้ และใน LG WOLED กับ Samsung QD-OLED มักมีคนชี้เรื่องสีเหลื่อม
- ปัญหานี้ยังมีช่องทางรับมือด้วย การปรับแก้ในซอฟต์แวร์ มากกว่าการเปลี่ยนฮาร์ดแวร์
คุณค่าเชิงปฏิบัติของการเรนเดอร์ glyph แบบเรียลไทม์
- UI ที่ดีและคุณภาพข้อความที่ดีช่วยยกระดับคุณภาพที่ผู้ใช้รับรู้ได้
- ในเกม องค์ประกอบที่ผู้ใช้มักจ้อง เช่น กล่องข้อความ เมนู ชื่อเรื่อง และการแจ้งเตือน ปรากฏอยู่บ่อย และคุณภาพข้อความที่ลดลงอาจกระทบประสบการณ์ได้พอ ๆ กับฉาก 3D ที่เรนเดอร์ได้แย่
- Persona series, Metaphor: ReFantazio, Nier: Automata เป็นตัวอย่างที่ดีของการนำเสนอ UI และข้อความ
- ความพยายามในการปรับปรุงคุณภาพการเรนเดอร์ glyph แบบเรียลไทม์มีคุณค่าจริงทั้งใน UI และการเรนเดอร์เกม
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ในวิดีโอแรก จุดของตัว j แบบเอียง มันเป็นอย่างไรเหรอ?
การเรนเดอร์ฟอนต์แบบ subpixel สำคัญต่อความอ่านง่าย แต่น่าเสียดายอย่างที่บทความชี้ไว้ว่าเราไม่สามารถดึงสเปกการจัดวางพิกเซลจากมาตรฐานจอภาพที่มีอยู่ได้
โลกค่อย ๆ ย้ายไปสู่ จอระดับ Retina มากขึ้น และในบริบทนั้นแทบไม่มีเหตุผลให้ใช้ subpixel rendering
มันมีปัญหาจุกจิกหลายอย่าง เช่น ภาพหน้าจอจะผูกกับการจัดวาง subpixel แบบเฉพาะ และการขยาย/ย่อบิตแมปก็ทำได้ยาก
มันเคยเป็นนวัตกรรมชั่วคราวในยุค LCD ระหว่าง CRT กับ Retina และตอนนี้ใกล้เคียงกับเทคโนโลยีที่มองย้อนอดีตมากกว่า Apple มีเหตุผลสมควรที่ถอดมันออกจาก macOS เมื่อหลายปีก่อน
สงสัยว่าผู้ผลิตจอไม่ได้นำไปใช้งานหรือเปล่า ไม่ว่าอย่างไร สำหรับรุ่นจอที่พบได้บ่อยที่สุด นี่เป็นข้อมูลที่อนุมานแล้วบันทึกไว้ใน ฐานข้อมูลข้อมูลฮาร์ดแวร์ ได้ง่าย
บทความยอดเยี่ยม และยังลิงก์ “subpixel zoo” ที่แสดงตัวอย่างหลากหลายไว้ด้วย: https://geometrian.com/resources/subpixelzoo/
แม้ในกรณีที่หลีกเลี่ยงไม่ได้อย่างมอนิเตอร์รายงานการจัดวางผิด ก็ยังต้องใช้วิธีแบบนี้อยู่ดี
แค่ฟอนต์บิตแมปที่ไม่มี anti-aliasing หรือฟอนต์เวกเตอร์ที่ hint แล้ว ก็อ่านได้ดีมาก
มันจะสำคัญขึ้นเฉพาะกับภาษาที่ตัวอักษรมีรายละเอียดซับซ้อนมากอย่างภาษาจีนหรือภาษาญี่ปุ่น
GTK4 ยอมทิ้ง RGB subpixel rendering ตอนย้ายการเรนเดอร์ไปที่ GPU
เคยได้ยินว่าการตัดสินใจที่เน้น GPU ทำให้การคง RGB subpixel rendering ไว้ทำได้ยาก แต่บทความนี้แสดงให้เห็นว่าทำได้
ถ้าอย่างนั้น เหตุผลของ GTK อาจอยู่ที่เรื่องอื่น หรือวิธีแก้ที่นำเสนอมีข้อเสีย หรืออาจผสานเข้ากับสแต็กเดิมได้ไม่ดี
ถ้าสนใจวิธี implement SDF และ MSDF ใน WebGL / WebGPU ดูบทเรียนที่ผมเขียนเองได้: https://infinitecanvas.cc/guide/lesson-015#msdf
ผมเคยลองย้ายตัวอย่าง JavaScript ไปเป็น Rust มาก่อน มัน copy/paste ตรง ๆ ไม่ได้ แต่ API คล้ายกันพอที่จะพอร์ตได้ง่าย จึงเหมาะกับการเรียนมาก
และยังทำให้คุ้นกับการใช้เอกสารของ WGPU ด้วย
ผมชอบทำบทเรียนเกี่ยวกับ GPU และอยากจัดโครงสร้างแบบนี้บ้าง สงสัยว่าเป็นเทมเพลตที่มีอยู่แล้วหรือเป็นส่วนหนึ่งของคอร์สอะไรหรือเปล่า
ไลบรารี Slug เป็นมิดเดิลแวร์เชิงพาณิชย์ที่ implement GPU glyph rasterizer แบบนี้
[1]: https://sluglibrary.com/
ถ้าทำเวอร์ชันโอเพนซอร์สบน wgpu โดยใช้บางส่วนของการ parse ฟอนต์และ layout จาก cosmic-text ก็น่าสนุกดี แต่ถ้าท้ายที่สุดโดน Slug ฟ้องคงไม่สนุกเลย
GPU ดูเหมือนมีความสามารถวาด vertex/pixel ได้แทบไม่จำกัด แล้วทำไมยังต้องเรนเดอร์ข้อความแบบ offline แล้วเก็บไว้ใน atlas แถมยังต้องใช้ทริกอย่าง SDF ด้วย ผมยังไม่เข้าใจ
ในบทความก็บอกว่าเขียนเส้นโค้ง glyph ลง atlas เลยสงสัยว่าทำไม shader ถึงเรนเดอร์ข้อความโดยตรงไม่ได้
น่าจะมีวิธีแปลง Bezier เป็น triangle mesh อยู่แน่ ๆ ผมกำลังจะเริ่มทำตัวเรนเดอร์ข้อความด้วย GPU สำหรับแอป CAD พอดี หวังว่าจะได้รู้เหตุผลในเร็ว ๆ นี้
GPU เร็วก็จริงแต่ไม่ได้เร็วไร้ขีดจำกัด และมันถนัดมากกับการ sample texture ที่เรนเดอร์ไว้ล่วงหน้า
ไม่ใช่แค่เรื่องความเร็ว แต่เป็นเรื่องการใช้พลังงานด้วย ถ้าถึงอัตรารีเฟรชของมอนิเตอร์แล้ว ประสิทธิภาพที่เพิ่มขึ้นจะไม่ทำให้การตอบสนองดีขึ้น แต่ช่วยให้แบตเตอรี่อยู่ได้นานขึ้นได้
ในการเรนเดอร์ไม่มีคำว่า “เร็วพอ” ยิ่งเร็วขึ้นก็ได้ประโยชน์เสมอ
สถาปัตยกรรม GPU สมัยใหม่ทั้งหมดไม่ถนัดกับการประมวลผล geometry ความหนาแน่นสูง ในกรณีแบบนี้ การยัดสามเหลี่ยมเข้า GPU ตรง ๆ จึงไม่มีประสิทธิภาพมากเมื่อเทียบกับ atlas หรือเทคนิคอื่น
GPU ส่วนใหญ่ dispatch pixel shader เป็นชุดละ 4 ถ้าสามเหลี่ยมทั้งหมดมีขนาด 1 พิกเซล shader thread 3 ตัวจะไม่ได้มีส่วนต่อผลลัพธ์ที่มองเห็น
สิ่งนี้เรียกว่า quad overdraw แถมยังเสียเวลาจำนวนมากกับการประมวลผล vertex โดยไม่มีเหตุผลจริง ๆ
ทำได้ก็จริง แต่เท่ากับยอมเสียส่วนหนึ่งของ frame budget และเพิ่มการใช้พลังงานโดยแทบไม่ได้ประโยชน์
ผู้เขียนใช้ atlas เพราะทำ supersampling เส้นโค้ง Bezier สูงสุด 512 sample ต่อพิกเซล ซึ่งแพงมาก
ถ้าเปลี่ยนเป็นการคำนวณอินทิกรัลของจุดตัดระหว่างพื้นที่เส้นโค้ง Bezier กับพื้นที่ subpixel น่าจะเร็วกว่าเยอะ และน่าจะรันแบบเรียลไทม์ได้โดยไม่ต้องใช้ atlas แถมแม่นยำกว่า supersampling ด้วย
และแทบทุกครั้งคุณก็อยากใช้เวลานั้นกับอย่างอื่น
ยิ่งต้องใช้เวลา GPU มากเท่าไร ฮาร์ดแวร์ขั้นต่ำที่ต้องการก็ต้องเร็วขึ้นเท่านั้น ข้อความนั้นสวยและสำคัญ แต่ก็อาจไม่ได้สำคัญถึงขั้นยอมเสียผู้ใช้หรือลูกค้า
ดูเหมือนแย่กว่าระดับที่ว่า “OLED รุ่นใหม่ ๆ ดูดี แต่มีปัญหาสีฟุ้งเพราะโครงสร้างซับพิกเซลไม่เป็นมาตรฐาน”
เท่าที่ผมเข้าใจ มันไม่ใช่แค่ไม่เป็นมาตรฐาน แต่ใน OLED มี รูปแบบการจัดวางซับพิกเซลหลายแบบ ที่ไม่เข้ากัน
ดังนั้น FreeType จึงไม่ได้ทำ subpixel rendering สำหรับ OLED และนี่ก็เป็นเหตุผลที่ถ้าต้องทำงานกับข้อความควรหลีกเลี่ยง OLED
ไม่ใช่ปัญหาของ FreeType อย่างเดียว เพราะ GUI toolkit อย่าง Qt, GTK ก็ต้องปรับให้เข้ากันด้วย ผมไม่แน่ใจว่ามีความคืบหน้าในการแก้ปัญหาแค่ไหน
ถ้าเข้าถึงโครงสร้างซับพิกเซลของจอภาพใด ๆ ได้ก็คงดี และข้อมูลแบบนี้อาจควรถูกส่งผ่าน EDID
เช่น แล็ปท็อปของผมเป็นแบบ BGR แนวตั้ง ซึ่ง FreeType กับ KDE รองรับได้ดี
ผมคิดว่าการจัดวางแปลก ๆ มักเกิดกับจอ HDR เพราะต้องใช้ขนาดต่างกันในแต่ละสี เพื่อไม่ให้บางสี โดยเฉพาะสีน้ำเงิน ไหม้เร็วเกินไป
เป็นงานที่น่าประทับใจมาก
สำหรับคนที่ไม่คุ้นกับสาขานี้ ขอเสริมว่า Valve เคยทำ SDF text rendering สำหรับเกม และตีพิมพ์งานวิจัยสำคัญในหัวข้อนี้เมื่อปี 2007
เทคนิคนี้ยังคงถูกใช้กันอย่างแพร่หลายในวิดีโอเกมแทบโดยไม่เปลี่ยนแปลงมากนัก
ในปี 2012 Behdad Esfahbod สร้าง Glyphy ซึ่งเป็นการใช้งาน SDF บน GPU ด้วย OpenGL ES และได้รับการยอมรับอย่างกว้างขวางเพราะประสิทธิภาพและความสามารถใหม่ ๆ อย่างการแปลงข้อความได้รวดเร็ว แต่ไม่ได้ถูกใช้อย่างแพร่หลาย
ระบบปฏิบัติการและเว็บเบราว์เซอร์สมัยใหม่กลับมักเลือกพึ่งพาการ rasterize แบบ TrueType สไตล์ทศวรรษ 1990 แทนเทคนิคเหล่านี้
เป็นแนวทางที่เบาและได้ผล แต่ดังที่บทความแสดงให้เห็น มันทำ subpixel alignment หรือการจัดวางซับพิกเซลตามอำเภอใจไม่ได้ การซูมเข้า/ออกมีต้นทุนด้านประสิทธิภาพสูง และการแปลงที่ซับซ้อนอย่างการเอียง การหมุน หรือการแปลง 3D ก็ทำในเอนจินเรนเดอร์ข้อความไม่ได้
หากต้องการข้อความที่ถูกหมุนหรือบิดรูป ก็ต้อง resample บิตแมป ซึ่งทำลายรายละเอียดเล็ก ๆ ทั้งหมดที่ช่วยให้อ่านง่าย ทำให้ดูไม่ดี
เหตุผลที่ความคืบหน้าช้าอาจเป็นเพราะปริมาณงานและความเสี่ยงสูงเกินไปเมื่อเทียบกับประโยชน์ ลองนึกถึงการเขียนเอนจินเว็บเบราว์เซอร์สมัยใหม่ใหม่ให้เป็น text rendering ที่เร่งด้วย GPU ก็คงไม่ใช่เรื่องง่าย
การเรนเดอร์ glyph เป็นเพียงส่วนหนึ่งเท่านั้น การจัดการตัดบรรทัดก็เป็นอีกปัญหาหนึ่ง อาจช้าเพราะต้องสื่อสารระหว่าง CPU กับ GPU มาก และการผสานซอฟต์แวร์กับ GPU อย่างลึกซึ้งก็ทำได้ยาก
วิธีนี้ให้ประสิทธิภาพดีกว่าการฝืนยัดให้เข้ากับ hardware 3D rendering pipeline แบบ SDF มาก
Safari ก็น่าจะเป็นเช่นนั้น แต่ผมยืนยันจากการตรวจสอบเองไม่ได้
ความคิดที่ว่าสถานะเทคโนโลยีล่าสุดหรือ implementation ที่ส่งถึงผู้ใช้ไม่ได้ก้าวหน้าไปนั้นไม่ถูกต้อง
SDF เข้ารหัสระยะทางเฉพาะที่ (Distance) จากพิกเซลที่กำหนดไปยังขอบตัวอักษร ในรูปของฟิลด์ (Field) ซึ่งเป็นอาร์เรย์ข้อมูลสองมิติ และใช้บิตเครื่องหมาย (Sign) เพื่อระบุว่าระยะนั้นอยู่ด้านในหรือด้านนอกตัวอักษร
ตัวอักษรแต่ละตัวมีแผนที่ข้อมูลขนาดเล็ก ถูกแพ็กเข้าด้วยกันในรูปไฟล์ภาพที่เป็นมิตรกับ GPU และถูกใช้โดย SDF rendering shader พร้อมไฟล์คำอธิบายที่บอกว่าจะหาภาพย่อยของแต่ละตัวอักษรได้ที่ไหน
นิยามตัวอักษรแบบนี้ทนต่อการ interpolation เชิงเส้นระหว่างค่าฟิลด์ได้ดีมาก จึงขยายได้เกือบสมบูรณ์แม้ใช้แผนที่ความละเอียดค่อนข้างต่ำ GPU เองก็ถนัดในการ interpolate ค่าพิกเซลของแผนที่อยู่แล้ว
แต่ประเด็นสำคัญคือ ต้อง preprocess แผนที่เหล่านี้ระหว่างการพัฒนา สำหรับทุกอักขระที่ต้องการเรนเดอร์จากระบบฟอนต์เดิม และต้องทำกับทุกอักขระที่ฟอนต์รองรับ
ข้อมูลน้อยกว่าการเรนเดอร์ทุกอักขระเป็นฟอนต์บิตแมปความละเอียดสูงมาก แต่ก็มากกว่านิยามเส้นโครงร่างของฟอนต์เองมาก
ระบบอย่างระบบปฏิบัติการหรือเบราว์เซอร์ที่ต้องการรองรับข้อความที่เป็นไปได้ทั่วโลกทั้งหมด ไม่สามารถใช้ SDF เป็นระบบเรนเดอร์ข้อความได้ เพราะจะต้องมี SDF map สำหรับ ชุดอักขระ Unicode ทั้งหมด ซึ่งใหญ่เกินไป
เกมเหมาะกับวิธีนี้ เพราะโดยทั่วไปไม่จำเป็นต้องรองรับการแปลภาษาให้ดีพอ หรือไม่จำเป็นต้องแสดงข้อความแบบกำหนดเองได้ทั้งหมด
เดิมที SDF ก็รองรับอีโมจิไม่ได้ด้วย เพราะเข้ารหัสเพียงระยะถึงขอบ glyph และไม่ได้เก็บข้อมูลสีภายใน glyph
มีเวอร์ชันปรับปรุงอย่าง Multichannel SDF ที่รองรับหลายสีได้ แต่จำนวนสีทั้งหมดก็มีข้อจำกัด
ในทางปฏิบัติ หากดูเกมที่ใช้ SDF กับข้อความในเกม และมีระบบแชตที่ชุมชนทั่วโลกโต้ตอบกันได้อย่างละเอียด ก็มีความเป็นไปได้สูงที่ข้อความในเกมกับข้อความในระบบแชตจะใช้การเรนเดอร์คนละแบบ
ตาม https://keithclark.co.uk/articles/gpu-text-rendering-in-webk... (2014) ระบุว่า “ใน Chrome, Safari หรือ Opera เวอร์ชันปัจจุบัน เมื่อ element ถูกเลื่อนขึ้นไปอยู่บน GPU จะสูญเสีย subpixel antialiasing และข้อความจะถูกเรนเดอร์แบบ grayscale”
ถ้าอย่างนั้นผมสงสัยว่าสิ่งที่ขาดไปคืออะไร จากประโยคนั้น อย่างน้อยบางส่วนของขั้นตอนจากสตริง UTF-8 ไปเป็นบิตแมปก็น่าจะทำบน GPU ได้ไม่ใช่หรือ
เป็นงานที่น่าประทับใจ
แต่โดยส่วนตัวคิดว่า subpixel anti-aliasing ไม่ได้มีความหมายมากนัก มันเป็นแฮ็กที่ใช้ได้ดีในยุคปี 2000 ที่ยังใช้จอ 72dpi แต่บนจอ Retina สมัยใหม่แทบสังเกตไม่ออก และต้องแลกข้อเสียหลายอย่างเพื่อการปรับปรุงที่เล็กมาก
ใช้ได้เฉพาะบนพื้นหลังทึบแสงเท่านั้น และไม่สามารถนำเอฟเฟกต์อย่างการปรับขนาด การสะท้อนภาพ หรือเบลอ ไปใช้กับผลลัพธ์ที่ rasterize แล้วได้ อีกทั้งเมื่อดูสกรีนช็อตบนจออื่นก็จะดูแย่ลง
ตามการสำรวจฮาร์ดแวร์ของ Firefox [1] ผู้ใช้ 16% ใช้จอแสดงผลความละเอียด 1366x768
นี่ไม่ใช่แค่ปัญหาของฮาร์ดแวร์เก่าเท่านั้น เพราะจอและโน้ตบุ๊ก 96dpi ก็ยังถูกผลิตอยู่ในปัจจุบัน
[1]: https://data.firefox.com/dashboard/hardware
เหตุผลอื่น ๆ ไม่ได้สำคัญมากนักเมื่อเทียบกับผลลัพธ์ที่ดีกว่าจาก subpixel rendering ในจุดที่ใช้ได้
สิ่งสำคัญคือ SDF คำนวณระยะของพิกเซลไปยังขอบที่ใกล้ที่สุด แต่ตัวเรนเดอร์ฟอนต์แบบดั้งเดิมคำนวณ coverage ของพิกเซล
coverage ของพิกเซลคือวิธีที่เหมาะที่สุด ในฟอนต์ขนาดเล็ก SDF อาจดูแย่ตรงบริเวณที่ขอบมาบรรจบกัน
บนจอ PPI สูงอาจเป็นปัญหาน้อยลงก็ได้ เคยทำ SDF renderer เองแล้ว แต่มันดูแย่กว่า FreeType
แต่ข้อสังเกตที่ว่า distance field ก่อปัญหาตรงจุดตัด หรือโดยทั่วไปคือตรงมุมคม ๆ นั้นถูกต้อง
สามารถบรรเทาได้ในระดับหนึ่งด้วยการใช้ distance field หลายชุดแล้วเรนเดอร์จุดตัดของมัน ตัวอย่างเช่น https://github.com/Chlumsky/msdfgen