1 คะแนน โดย GN⁺ 2024-11-11 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ในสถานการณ์ที่ต้องพิมพ์ข้อความดีบักอย่างรวดเร็วภายใน render pass การเตรียม font atlas อาจเป็นภาระ ดังนั้นวิธีวาดข้อความด้วย ค่าคงที่ใน fragment shader เพียงอย่างเดียวจึงมีประโยชน์
  • glyph แสดงเป็นบิตแมป 8x16 พิกเซล และเก็บอักขระ printable ASCII 96 ตัวไว้ในอาร์เรย์ uvec4 ขนาด 1536 ไบต์ เพื่อให้ shader ค้นหาได้โดยตรง
  • ข้อมูลฟอนต์สามารถสร้างได้จาก ฟอนต์เทอร์มินัล PSF1 โดยข้าม header 4 ไบต์และช่วง glyph ที่ไม่ใช่อักขระแสดงผล 512 ไบต์ จากนั้นดึง glyph 96 ตัวถัดไปออกมา
  • การเรนเดอร์ใช้ uint32_t หน่วยละ 4 ตัวอักษร และโครงสร้าง per-instance ที่เก็บตำแหน่งกับสเกล เพื่อวาดข้อความทั้งหมดด้วย instanced draw call เดียว
  • เป็นเทคนิคที่เน้นการแสดงผลดีบักแบบง่าย ๆ จึงต้องจัดการข้อจำกัดต่าง ๆ ร่วมกันทั้งฝั่งแอปพลิเคชันและ shader เช่น padding ทีละ 4 ตัวอักษร, อักขระ fill \0 ที่ใช้ discard, และ การชดเชย endian

ใช้ค่าคงที่ใน shader แทน font atlas

  • การเรนเดอร์ข้อความทั่วไปคือการเรนเดอร์ glyph ที่เป็นไปได้ลงใน font atlas, bind เป็นเท็กซ์เจอร์ แล้ววาดสามเหลี่ยมที่เหมาะกับแต่ละ glyph
  • imgui และ stb_truetype ก็ใช้วิธีนี้เช่นกัน แต่สำหรับการแสดง ข้อความดีบัก อย่างรวดเร็ว ขั้นตอนเตรียมการอาจยุ่งยาก
  • ทางเลือกคือเก็บข้อมูลที่เทียบเท่ากับ font atlas เป็น ค่าคงที่จำนวนเต็มภายใน fragment shader
  • จำนวนเต็มสามารถใช้เหมือนบิตแมปได้ โดย map ตำแหน่ง xy ของ fragment ไปยังตำแหน่งบิตเฉพาะ แล้วถ้าบิตนั้นเปิดอยู่ให้แสดงสี foreground ถ้าปิดอยู่ให้แสดงสี background

glyph บิตแมป 8x16 และตาราง ASCII

  • หนึ่งไบต์แสดงพิกเซลได้เพียงหนึ่งแถว ดังนั้นเพื่อให้ glyph อ่านง่ายขึ้นจึงใช้ 16 ไบต์ ต่อ glyph
  • glyph หนึ่งตัวเป็น canvas ขนาด 8x16 พิกเซล และ uvec4 หนึ่งตัวของ GLSL สามารถเก็บ 16 ไบต์ที่ต้องใช้ได้พอดี
  • เมื่อเก็บ glyph แบบ printable ASCII 96 ตัว ข้อมูลทั้งหมดจะมีขนาด 1536 ไบต์
  • อาร์เรย์ font_data[96] ใช้ค่าที่นำค่า ASCII ไปลบ 0x20 เป็น index
    • ครอบคลุม glyph printable ASCII ที่เริ่มจาก 0x20 SPACE
    • ในโค้ดตัวอย่างแสดงเพียงบางรายการเพื่อประหยัดพื้นที่
  • ตารางบิตแมปฉบับเต็มรวมอยู่ใน ซอร์สโค้ด Island

ดึงบิตแมปจากฟอนต์ PSF1

  • การเข้ารหัสบิตแมปที่ต้องใช้นั้นแทบจะตรงกับฟอนต์เทอร์มินัลใน รูปแบบ PSF1
  • ขั้นตอนดึงข้อมูลจากฟอนต์เทอร์มินัล PSF1 นั้นเรียบง่าย
    • เปิดไฟล์ฟอนต์ด้วย hex editor อย่าง ImHex
    • ข้าม header 4 ไบต์
    • ข้ามส่วน glyph ที่ไม่ใช่อักขระแสดงผลขนาด 512 ไบต์
    • ส่งออก glyph 96 ตัวถัดไป หรือก็คือ 1536 ไบต์ ด้วย “Copy as → C Array”
  • อาร์เรย์ char ที่ดึงออกมาสามารถแก้ไขเป็นอาร์เรย์ uint แล้วจัดกลุ่มเป็นหน่วย uvec4 ได้
  • หากนำ raw char มาต่อกันเป็น uint ตรง ๆ endian จะกลับด้าน จึงต้องชดเชยกลับตอน sampling
  • ข้อมูลต้นฉบับของฟอนต์พิกเซลที่ใช้มาจากฟอนต์พิกเซลฟรี Tamsyn ของ Scott Fial

การประกอบเป็น instanced draw call เดียว

  • การเรนเดอร์ข้อความทำด้วย instanced draw call เพียงรายการเดียว
  • draw call ใช้ attribute stream สองชุด
    • stream แบบ per-draw มีเฉพาะข้อมูลที่จำเป็นในการวาด quad ทั่วไป
    • stream แบบ per-instance เก็บ offset ตำแหน่งบนหน้าจอและข้อความที่จะแสดง
  • offset ตำแหน่งใช้ค่า float สำหรับ x, y และพื้นที่ float ที่เหลือสามารถใส่ค่าสเกลของฟอนต์ได้
  • ใน Vulkan คอมโพเนนต์ทั้งหมดของ vertex output binding ต้องมีลักษณะ interpolation เดียวกัน จึงผสม vec3 กับ uint ใน binding เดียวกันให้เรียบร้อยได้ยาก
  • ข้อความถูกแพ็กเป็น uint32_t ในหน่วย 4 ตัวอักษร
    • หน่วยขั้นต่ำของชนิดข้อมูล vertex attribute พื้นฐานมักเป็น 32 บิต จึงใส่ 4 อักขระพร้อมกัน
    • ความยาวข้อความต้องหารด้วย 4 ลงตัว
    • ส่วนที่ขาดเติมด้วยอักขระ \0
  • ข้อมูล per-instance แสดงด้วยโครงสร้าง word_data
    • pos_and_scale[3]: ตำแหน่ง xy และ scale
    • word: ตัวอักษรสี่ตัวที่จะพิมพ์
  • แอปพลิเคชันแบ่งข้อความเป็น chunk ละ 4 ตัวอักษร แปลงแต่ละ chunk เป็น uint32_t แล้วสะสมลงในอาร์เรย์ word_data พร้อมกับ offset ตำแหน่ง
  • ตอนเรนเดอร์ ให้นำอาร์เรย์นี้ไป bind เป็น per-instance binding ของ debug text drawing pipeline แล้ววาด instance ตามจำนวน quad

ส่งต่อตำแหน่งและตัวอักษรใน Vertex Shader

  • vertex shader ส่งออก gl_Position, word ที่จะเรนเดอร์ และค่าที่เทียบเท่าพิกัดเท็กซ์เจอร์
  • gl_Position ใช้ข้อมูล pos_and_scale แบบ per-instance เพื่อวางจุดยอดสามเหลี่ยมบนหน้าจอในระบบพิกัด NDC
  • word ที่จะเรนเดอร์ส่งต่อ attribute input ชนิด uint ไปยัง fragment shader ตรง ๆ
    • ใช้ qualifier flat เพื่อไม่ให้ถูก interpolate
  • พิกัดเท็กซ์เจอร์สังเคราะห์จาก gl_VertexIndex
    • 12 >> gl_VertexIndex & 1 สร้างลำดับ 0, 0, 1, 1
    • 9 >> gl_VertexIndex & 1 สร้างลำดับ 1, 0, 0, 1
    • การผสมนี้สร้างพิกัด uv (0,1), (0,0), (1,0), (1,1) ได้โดยไม่ต้อง branch
  • vertex shader ยังรับสี foreground และ background เป็นข้อมูล per-instance แล้วส่งต่อให้ fragment shader

การ sampling glyph ใน Fragment Shader

  • เพื่อเรนเดอร์ข้อความ fragment shader ต้องใช้ข้อมูลสามอย่าง
    • พิกัด uv ของ fragment ที่ผ่านการ interpolate
    • ข้อมูลตัวอักษรที่จะพิมพ์ in_word
    • อาร์เรย์บิตแมป glyph font_data
  • พิกัด uv อยู่ในช่วง float แบบ normalized ตั้งแต่ vec2(0.f,0.f) ถึง vec2(1.f,1.f) และพิกัดพิกเซลของ glyph อยู่ตั้งแต่ uvec2(0,0) ถึง uvec2(7,15)
  • word 4 ตัวอักษรทั้งหมดถือเป็นพื้นที่กว้าง 32 พิกเซล สูง 16 พิกเซล
    • ใช้ floor กับ uv.xy * vec2(8 * WORD_LEN, 16) เพื่อ quantize เป็นพิกัดพิกเซลของ word
    • จำกัดช่วงพิกัดเป็น uvec2(0..31, 0..15)
    • ใช้ word_pixel_coord.x / 8 เพื่อหาว่าเป็นพื้นที่ของตัวอักษรใดในสี่ตัว
    • ใช้ word_pixel_coord.x % 8 เพื่อหาพิกัด x ภายใน glyph
  • รหัสอักขระถูกแปลงเป็น index ของ font_data
    • เนื่องจาก glyph ตัวแรกคือ 0x20 SPACE จึงใช้ printable_character - 0x20 เป็น offset
    • ใช้ offset นั้นดึงบิตแมป glyph แบบ uvec4
  • พิกัด y ใช้ glyph_pixel_coord.y / 4 เพื่อเลือก uint เฉพาะตัวใน uvec4
    • uint นี้เก็บข้อมูลพิกเซล 4 แถว
    • เนื่องจากนำ char ที่ดึงจาก ImHex มาต่อกันเป็น uint ตรง ๆ ลำดับแถวจึงกลับด้าน
    • ชดเชยด้วยการ index จากด้านหลังในรูปแบบ (8*(3-(glyph_pixel_coord.y)%4))
  • พิกัด x เลือกบิตด้วย 7-glyph_pixel_coord.x
    • บิตที่มีค่าสูงสุดของไบต์ถูกเก็บไว้ที่ index สูงสุด จึงต้อง index แบบย้อนกลับเพื่อให้สอดคล้องจากซ้ายไปขวา
  • ใช้ค่า current_pixel สุดท้ายกับ mix(background_colour, foreground_colour, current_pixel) เพื่อกำหนดสี

การจัดการสตริงสั้นและอักขระ fill

  • หากความยาวสตริงหารด้วย 4 ไม่ลงตัว แอปพลิเคชันจะเติมส่วนที่ขาดด้วยอักขระ \0
  • fragment shader ตรวจสอบว่าอักขระที่จะพิมพ์เป็น \0 หรือไม่
  • เมื่อพบอักขระ fill \0 จะไม่วาดแม้แต่ background และเรียก discard
  • การจัดการนี้ทำให้ยังคงข้อจำกัดการแพ็กทีละ 4 ตัวอักษรไว้ได้ พร้อมกับแสดงสตริงสั้นได้

รูปแบบการใช้งานและตำแหน่งโค้ด

  • ในโปรเจกต์ Island สามารถเรียก le::DebugPrint เพื่อแสดงข้อความดีบักบนหน้าจอได้
  • โค้ด fragment shader ฉบับเต็มดูได้ที่ github
  • ตัวอย่างโค้ดส่งข้อมูลสตริงเพื่อแสดงข้อความในรูปแบบ "That's all, %s" บนหน้าจอ

1 ความคิดเห็น

 
GN⁺ 2024-11-11
ความคิดเห็นจาก Hacker News
  • ถ้าอยากลองทำเอง ก็ไล่ตามคณิตศาสตร์ไปได้เลย การทำตั้งแต่ศูนย์ใน ShaderToy ง่ายมาก และถ้าคุณชอบอะไรแบบนี้ก็สนุกดีในเช้าวันเสาร์
    การทำเองตั้งแต่ต้นสนุกอยู่แล้ว แต่ถ้าต้องการคำใบ้สำหรับเริ่มต้น นี่คือตัวอย่างที่เพิ่งทำขึ้นมา: https://www.shadertoy.com/view/Mc3cW2
    ยังมีแฮ็กข้อความฉลาด ๆ ที่คนอื่นทำไว้อีกมาก เช่น ตัวอย่าง Matrix ที่สั้นกว่า 300 ตัวอักษร https://www.shadertoy.com/view/llXSzj หรือเอฟเฟกต์จอ CRT สีเขียว https://www.shadertoy.com/view/XtfSD8

    • ทุกครั้งที่ลองทำ การเรนเดอร์ข้อความแบบ immediate mode ผมทำให้ข้อความดูดีในขนาดเล็กไม่ได้เลย แม้แต่ ShaderToy ตัวแรก ถ้าเปลี่ยน 30 ใน vec2(30, -30) เป็น 300 ก็จะเห็นอาร์ติแฟกต์
      สงสัยว่ามีเคล็ดลับในการจัดการให้ถูกต้องไหม กรณีของผม การทำ multi-sampling เท็กซ์เจอร์ภายใน fragment shader ได้ผลดีที่สุด แต่ก็ยังไม่ดีเท่าระดับสมัยใหม่
    • ผมพัฒนาเกมด้วย Unity และด้านนี้ให้ความรู้สึกเหมือนถอยหลังจริง ๆ เมื่อหลายปีก่อน Unity ซื้อ เครื่องมือเรนเดอร์ฟอนต์ ที่ดีที่สุดซึ่งคนคนเดียวทำไว้ แล้วเอามาใส่เป็นฟีเจอร์พื้นฐาน หลังจากนั้นการพัฒนาก็แทบหยุด และตลาดเครื่องมือเรนเดอร์ฟอนต์คู่แข่งก็ตายไปด้วย
      ไม่นานมานี้ผมลองทำแอปที่ดูเหมือนฟอนต์คอนโซลแบบ native แต่ต้องนั่งปรับอยู่นานกว่า 2 ชั่วโมงกว่าจะได้ใกล้เคียงประมาณ 90%
    • นอกประเด็นแต่เป็นตัวอย่างที่น่าสนใจ เอฟเฟกต์ Matrix ที่ทำด้วย HTML/CSS/JS ใส่ลงใน 1024 ไบต์ได้: https://codegolf.stackexchange.com/a/17414
  • แปลกใหม่และแฮ็ก ๆ ดี เลยสนุก จริง ๆ แล้ว เทคนิคการเรนเดอร์ 3D แทบทั้งหมดก็เป็นแบบนั้น แต่ถ้าผลลัพธ์ไม่ได้ตั้งใจจะจำลอง BBS ยุคเก่า มันก็ไม่ได้สวยงามเป็นพิเศษ
    ถ้าเพิ่มบิตเข้าไปอีกก็อาจปรับปรุงได้ แต่ก่อนที่มันจะดูดีขึ้นมาก คุณคงหาวิธีที่ง่ายกว่าสำหรับกำหนดบิตทั้งหมดได้แล้ว สุดท้ายก็ย้อนกลับมาที่จุดเริ่มต้น เพราะแทบไม่มีวิธีไหนมีประสิทธิภาพกว่าการวาดเป็นพิกเซลขาวดำในโปรแกรมวาดภาพแล้วบันทึกเป็นเท็กซ์เจอร์
    ถ้าอยากรู้ว่าวิธีทั่วไปกว่าที่เอนจินเรนเดอร์ 3D สมัยใหม่ใช้วาดข้อความคืออะไร ลองค้นหา SDF text และเทคนิคที่เกี่ยวข้องอย่าง MSDF ดู เป็นวิธีที่ใช้ texture atlas แบบดั้งเดิมในขั้นตอน preprocessing เพื่อสร้าง atlas ของ signed distance field

    • พูดถึง “ย้อนกลับมาที่จุดเริ่มต้น” ถ้ายังไม่เคยเห็น บทความปี 1968 เรื่อง On the Design of Display Processors ก็น่าอ่าน: http://cva.stanford.edu/classes/cs99s/papers/myer-sutherland...
      บทความนั้นพูดถึงฮาร์ดแวร์ แต่ซอฟต์แวร์ก็มีวัฏจักรเวียนว่ายเหมือนกัน
    • วิธีอย่าง SDF text หรือ MSDF ตอนนี้ก็เก่าไปอย่างน้อยหนึ่งเจเนอเรชันแล้ว ทุกวันนี้แทบทั้งหมดใช้แนวทางแบบ https://sluglibrary.com คือ rasterize เส้นโค้ง Bezier ของฟอนต์โดยตรงใน shader
      เมื่อก่อนผมเคยทำเวอร์ชันพื้นฐานมาก ๆ ของแนวคิดนี้ไว้: https://www.shadertoy.com/view/sdXBDs
    • เช่น ในสถานการณ์ที่อัปโหลดเท็กซ์เจอร์ไม่ได้อย่างถูกต้อง วิธีนี้ค่อนข้างฉลาดสำหรับ debug text แต่ที่ต้นฉบับเปรียบ sprite sheet กับการเรียงพิมพ์ด้วยมือในศตวรรษที่ 16 นั้นก็น่ารักดี แต่ในความเป็นจริง ผู้ช่วยช่างพิมพ์ใช้เวลาหนึ่งชั่วโมงในการเรียงตัวพิมพ์โลหะ ขณะที่การอัปโหลด sprite sheet ไปยัง GPU ใช้ไม่ถึง 10ms และหลังจากนั้นก็ตั้งค่าได้ไม่จำกัด
      ไม่ได้หมายความว่าไม่ใช่ทริกที่เจ๋งนะ มันเป็นทริกที่เจ๋งจริง ๆ
  • ยังมีทางเลือกในการเรนเดอร์ข้อความเป็น mesh ด้วย TextMeshPro ไปไกลกว่านั้นอีกขั้น โดยใช้ signed distance field เพื่อรองรับการสเกลได้ตามต้องการ
    https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...

    • ถ้าไปไกลกว่านั้น ยังทำได้ด้วยการประเมินเส้นโค้งของฟอนต์โดยตรงบน GPU ซึ่งให้คุณภาพสูงได้ไม่ว่าจะสเกลหรือมุมมอง perspective เป็นอย่างไร การทำให้มีประสิทธิภาพนั้นยากมาก แต่ก็เป็นไปได้
      ตัวอย่าง: https://sluglibrary.com
      mesh และ SDF ทำให้ฝั่ง GPU ง่ายกว่ามาก แต่ถ้าขยายใหญ่เกินไปความแม่นยำอาจลดลง และถ้าย่อ mesh เล็กเกินไปก็อาจเกิด aliasing ได้
  • เจ๋งมาก ถ้ามี การเปรียบเทียบประสิทธิภาพ กับวิธีใช้เท็กซ์เจอร์แบบ “ดั้งเดิม” ก็น่าสนุกดี
    เวลาทำงานง่าย ๆ แบบนี้บน GPU ยุคใหม่ คำตอบของคำถามว่า “ประสิทธิภาพเป็นยังไง?” มักจะเป็น “ก็ใช้ได้”

    • คำตอบที่ผมอยากได้สำหรับ “ประสิทธิภาพเป็นยังไง?” คือ “VSCode จะไม่กิน VRAM ของผมเพิ่มอีกตั้งแต่หลายร้อย MB ไปจนถึงหลาย GB”
  • Sebastian Lague ทำวิดีโอดี ๆ ที่พูดถึง เทคนิคการเรนเดอร์ฟอนต์ หลายแบบไว้
    https://youtu.be/SO83KQuuZvg

  • ผมเคยลองเทคนิคคล้าย ๆ กันมาก่อน โดยใส่ข้อมูลฟอนต์ทั้งหมดไว้ในซอร์สโค้ดของ fragment shader เลย แบบนั้นจะสามารถใช้ snprintf พิมพ์ออกไปยังบัฟเฟอร์ GPU ที่แมปกับ CPU ได้โดยตรง ผมรู้ว่าเป็นวิธีที่เสี่ยง
    แทนที่จะวาดอักขระแต่ละตัวด้วย vertex shader ก็วาดแค่สามเหลี่ยมเต็มหน้าจอหนึ่งรูป แล้วใช้ gl_FragCoord แทนพิกัด UV ไม่ใช่วิธีที่มีประสิทธิภาพที่สุด แต่เป็นฟีเจอร์ดีบักและในทางปฏิบัติก็เร็วพอ
    ถึงชื่อไฟล์จะเป็นอย่างนั้น แต่ไม่ได้ใช้ฟอนต์ของ NES ใช้ฟอนต์จาก ROM ของ IBM PC แทน ส่วน “NES font” และฟอนต์พิกเซล 8x8 แบบอื่น ๆ หาได้บนเว็บ
    https://github.com/rikusalminen/triangles/blob/nesfont/shade...

    • ชุดฟอนต์พิกเซลที่ผมชอบที่สุดคืออันนี้: https://int10h.org/oldschool-pc-fonts/
    • เพิ่งรู้เมื่อไม่นานมานี้ว่า ฟอนต์ “NES” มาจากเกมอาร์เคด Quiz Show ปี 1976 ฟอนต์นี้ยังถูกใช้ในเกมขาวดำของ Kee/Atari เกมอื่น ๆ ด้วย และข้อมูลฟอนต์อยู่ในชุด ROM ของ quizshow สำหรับ MAME แต่ไม่รู้ทำไมถึงถูกแยกเป็นหน่วย nibble
      เกมนี้น่าสนใจตรงที่เก็บข้อมูลคำถามและคำตอบไว้ในเทป 8-track
  • เจ๋งดี ไม่ค่อยได้เห็น อัลกอริทึมเรนเดอร์ข้อความที่ผมยังไม่เคยลองเองมาก่อน ที่สตาร์ทอัพของผมเราเคยทำหลายแบบ แต่เพราะผมต้องการความเป็นอิสระจากความละเอียดหน้าจอและ anti-aliasing วิธีนี้คงไม่ช่วยเท่าไร
    มันอาจไม่สามารถทำให้ใช้ได้ทั่วไปกับไฟล์ฟอนต์เส้นโค้ง Bézier ทุกแบบ การแปลงเส้นโค้งเป็นพิกเซลอาจยาก โดยเฉพาะเมื่อ glyph ตัดกับตัวเอง โดยรวมแล้วการเรนเดอร์ข้อความแบบมาตรฐานให้ความรู้สึกว่าเป็นปัญหาที่แก้ได้แล้ว ส่วนกรณีใช้งานที่ไม่มาตรฐานนั้นทดลองทำได้โหดมาก
    แนวคิดของวิธีนี้ดูคล้ายกับวิธีของ Will Dobbie ที่ผมชอบ เพียงแต่เรียบง่ายกว่ามาก ทั้งสองรับข้อมูลฟอนต์ดิบแล้วใช้ใน shader โดยตรง ความต่างคือวิธีนี้เก็บข้อมูลพิกเซลไว้ในอาร์เรย์ ส่วน Will เก็บข้อมูล path ของ SVG เป็น “vector texture”
    ถ้าสนใจ Will มีเดโมที่ยอดเยี่ยมอยู่ที่นี่: https://wdobbie.com/warandpeace/

  • เมื่อก่อนผมเคยคิดจะลองทำอะไรแบบนี้ แต่เข้าใจว่า GPU มีประสิทธิภาพเป็นพิเศษกับการเรนเดอร์ texture และค่อนข้างช้ากับ การจัดการบิต ถึงตรงนี้จะประหยัดหน่วยความจำได้นิดหน่อย แต่สงสัยว่าจริง ๆ แล้วจะเร็วกว่าใช้ atlas หรือไม่
    ถ้าทำ bit packing ไว้ใน texture ธรรมดา แล้วให้ fragment shader ถอดรหัส ก็อาจได้ข้อดีทั้งสองด้าน

    • ความเข้าใจนั้นค่อนข้างเก่าแล้ว บน GPU ในช่วงราว 15 ปีหลังนี้ การ lookup texture ช้ากว่าการดำเนินการกับบิตประมาณ 100 เท่า
  • ถามด้วยความไม่ค่อยรู้เรื่องกราฟิกคอมพิวเตอร์สมัยใหม่เท่าไรนะครับ ค่าใช้จ่ายในการอัปโหลด texture ขนาดเล็กขึ้น GPU มันสูงขนาดนั้นเลยหรือ? สงสัยว่าทำไมไม่เรนเดอร์สตริงทั้งหมดเป็น texture 2D แล้วแสดง texture นั้นบนสามเหลี่ยมสองรูป

    • ค่าใช้จ่ายไม่ได้สูง วิธีนี้ใกล้เคียงกับการทำให้ ข้อความสำหรับดีบัก ปรากฏบนหน้าจอให้ง่ายที่สุด มากกว่าเรื่องประสิทธิภาพ แค่เพิ่มข้อมูลเล็กน้อยใน shader ข้อความก็ออกมาได้ทันที
      ในทางกลับกัน คุณต้องเขียนโค้ดสร้าง font atlas หรือหา atlas ที่มีอยู่แล้วมาโหลด และนั่นก็ต้องมีโค้ดโหลดด้วย หรือไม่ก็ต้องวาดข้อความทั้งข้อความลงใน texture แล้ว cache ผลลัพธ์ไว้จนกว่าข้อความจะเปลี่ยน
      นอกจากนี้ยังต้องจัดการ resource และ binding ด้วย แต่วิธีนี้ไม่ต้องใช้ resource กล่าวอีกอย่างคือไม่ใช่โซลูชันข้อความทั่วไป แต่เป็นเทคนิคสำหรับแสดงข้อความดีบักบนหน้าจอ
      อ้างอิงเพิ่มเติม เบราว์เซอร์และ OS ส่วนใหญ่ทำงานด้วยวิธีวาดข้อความลง texture โดยวาดฟอนต์แบบไดนามิกลงใน texture atlas แล้วใช้ glyph จาก atlas นั้นสร้าง texture เพิ่มสำหรับบางส่วนของหน้าต่างแอป ถ้าเปิดแสดงขอบเขต texture ในเบราว์เซอร์ คุณจะเห็น texture ทั้งหมด และ Rendering->Layer borders จะตีกรอบ texture แต่ละอันด้วยสีฟ้าอมเขียว
    • ทุกวันนี้โดยทั่วไปก็ยังควรหลีกเลี่ยงการเปลืองหน่วยความจำ GPU มากเกินไป texture กล่องข้อความขนาดใหญ่ต้องถูกส่งผ่าน PCI bus และอาจเกิดการหยุดชะงักได้ ขึ้นกับจังหวะการอัปโหลดหรือว่า resource ของ GPU ถูกขับออกหรือไม่
      ถ้า CPU ที่ค่อนข้างช้าต้องเรนเดอร์กล่องข้อความแบบ texture อิสระจำนวนมาก ก็จะสะสมอย่างรวดเร็วและกินงบประสิทธิภาพ
      การวาดโดยใช้ glyph atlas ยังคงดีกว่ามากในแง่การใช้ resource pipeline เรนเดอร์ข้อความสมัยใหม่มักใช้ SDF หรือเส้นโค้ง Bézier ที่เข้ารหัสไว้ เพื่อเพิ่มความอ่านง่ายของ glyph ตอนซูมเข้าออก และนั่นก็เป็นวิธีที่ดีในการประหยัดหน่วยความจำเช่นกัน
    • การวาดสี่เหลี่ยมหนึ่งรูปที่ครอบคลุมอักขระ N ตัว แล้วให้ shader เลือก glyph ออกมา น่าจะเร็วกว่าการวาดสี่เหลี่ยมแยกกันสำหรับอักขระแต่ละตัว อย่างน้อยก็สำหรับ ฟอนต์ความกว้างคงที่ อย่างไรก็ตาม จำนวนอักขระที่สามารถเติมเต็มหน้าจอได้ก็มีขีดจำกัด ดังนั้นความต่างจริง ๆ อาจไม่มาก
      ในมุมของการอัปโหลด สุดท้ายแล้วก็มี glyph ขนาด X ไบต์ และต้องเข้าไปอยู่ในหน่วยความจำ GPU ด้วยวิธีใดวิธีหนึ่ง ไม่ว่าจะเป็น texture, uniform data หรือ shader constant ก็แทบไม่ต่างกันมากในแง่ประสิทธิภาพ กลับกัน ถ้าใส่เป็น shader constant เหมือนต้นฉบับ ตัว compiler ของ shader ต้องประมวลผลการประกาศ constant ทั้งหมด ซึ่งอาจทำให้ฝั่ง CPU มีค่าใช้จ่ายสูงกว่า
      สิ่งสำคัญฝั่ง GPU คือเมื่ออ่านข้อมูล glyph แล้วไปแตะ memory hierarchy ชั้นไหน การ fetch texture ใช้ L1 cache เฉพาะบน GPU ส่วนใหญ่ และน่าจะใหญ่กว่า L1 cache ทั่วไป ลำดับข้อมูลก็สำคัญเช่นกัน โดยปกติ texture จะถูกเก็บในรูปแบบดัดแปลงของ Morton order เพื่อเลี่ยง cache miss ตอน shading บล็อกพิกเซล ถ้าเป็น renderer ข้อความที่ใช้ atlas ในงานจริง ก็ควรใช้ texture
      แก้ไข: ผมอ่านคำถามผิด ถ้าเปรียบเทียบระหว่างการวาด glyph แยกบน GPU กับการวาดบล็อกข้อความทั้งหมดบน CPU นั่นคือการแลกระหว่างความเร็วกับพื้นที่ คำตอบจะขึ้นกับว่าจะใช้หน่วยความจำกับข้อความมากแค่ไหน ข้อความเปลี่ยนหรือไม่ ต้องการเอฟเฟกต์รายตัวอักษรหรือเปล่า ฯลฯ
    • คุณสามารถเรนเดอร์สตริงทั้งหมดก่อนอัปโหลดได้ แต่แบบนั้นโดยแก่นแล้วเท่ากับใช้ CPU rendering ซึ่งน่าจะช้ากว่าให้ GPU ทำงานเดียวกัน
      อีกอย่าง ถึงจะเรียกว่า “ไม่มี texture” แต่วิธีนี้ก็ยังเป็น texture อยู่ดี เพียงแต่ texture ถูกเก็บในรูปแบบอื่นและตำแหน่งอื่น การเรนเดอร์ฟอนต์แบบไม่มี texture จริง ๆ คือการประเมินเส้นโค้งเวกเตอร์แบบทันที
    • แล้วแต่แอปพลิเคชัน โดยเฉพาะถ้าอาจเจอ ตัวอักษรที่เขียนจากขวาไปซ้าย, CJK, หรืออีโมจิ นี่เป็นวิธีที่ง่ายที่สุด ข้อความส่วนใหญ่ไม่ได้เปลี่ยนทุกเฟรม ดังนั้นการ cache texture จึงคุ้ม และสำหรับเราก็ดีพอแล้ว
  • ทั้งที่บอกว่าจะไม่เก็บบิตแมปไว้ในเชดเดอร์ แต่กลับอธิบายวิธี เก็บบิตแมปไว้ในเชดเดอร์ อย่างชัดเจน เลยค่อนข้างสับสน
    สรุปก็คือเป็นการฝังฟอนต์บิตแมปไว้ในเชดเดอร์

    • ไม่ใช่ เขาบอกว่าจะไม่เก็บบิตแมปไว้ในเท็กซ์เจอร์ต่างหาก ซึ่งไม่เหมือนกับการฝังลงในโค้ดเชดเดอร์โดยตรง
      เปรียบได้กับความต่างระหว่างการเก็บข้อมูลไว้ในไฟล์แยกที่ต้องอ่านตอนรันไทม์ กับการใส่ข้อมูลไว้ในซอร์สโค้ดโดยตรง