เรนเดอร์ข้อความโดยไม่ใช้เท็กซ์เจอร์
(poniesandlight.co.uk)- ในสถานการณ์ที่ต้องพิมพ์ข้อความดีบักอย่างรวดเร็วภายใน 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 ที่เริ่มจาก
0x20SPACE - ในโค้ดตัวอย่างแสดงเพียงบางรายการเพื่อประหยัดพื้นที่
- ครอบคลุม glyph printable ASCII ที่เริ่มจาก
- ตารางบิตแมปฉบับเต็มรวมอยู่ใน ซอร์สโค้ด 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_datapos_and_scale[3]: ตำแหน่งxyและ scaleword: ตัวอักษรสี่ตัวที่จะพิมพ์
- แอปพลิเคชันแบ่งข้อความเป็น 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
- ใช้ qualifier
- พิกัดเท็กซ์เจอร์สังเคราะห์จาก
gl_VertexIndex12 >> gl_VertexIndex & 1สร้างลำดับ0, 0, 1, 19 >> 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 ตัวแรกคือ
0x20SPACEจึงใช้printable_character - 0x20เป็น offset - ใช้ offset นั้นดึงบิตแมป glyph แบบ
uvec4
- เนื่องจาก glyph ตัวแรกคือ
- พิกัด y ใช้
glyph_pixel_coord.y / 4เพื่อเลือกuintเฉพาะตัวในuvec4uintนี้เก็บข้อมูลพิกเซล 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 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าอยากลองทำเอง ก็ไล่ตามคณิตศาสตร์ไปได้เลย การทำตั้งแต่ศูนย์ใน ShaderToy ง่ายมาก และถ้าคุณชอบอะไรแบบนี้ก็สนุกดีในเช้าวันเสาร์
การทำเองตั้งแต่ต้นสนุกอยู่แล้ว แต่ถ้าต้องการคำใบ้สำหรับเริ่มต้น นี่คือตัวอย่างที่เพิ่งทำขึ้นมา: https://www.shadertoy.com/view/Mc3cW2
ยังมีแฮ็กข้อความฉลาด ๆ ที่คนอื่นทำไว้อีกมาก เช่น ตัวอย่าง Matrix ที่สั้นกว่า 300 ตัวอักษร https://www.shadertoy.com/view/llXSzj หรือเอฟเฟกต์จอ CRT สีเขียว https://www.shadertoy.com/view/XtfSD8
vec2(30, -30)เป็น 300 ก็จะเห็นอาร์ติแฟกต์สงสัยว่ามีเคล็ดลับในการจัดการให้ถูกต้องไหม กรณีของผม การทำ multi-sampling เท็กซ์เจอร์ภายใน fragment shader ได้ผลดีที่สุด แต่ก็ยังไม่ดีเท่าระดับสมัยใหม่
ไม่นานมานี้ผมลองทำแอปที่ดูเหมือนฟอนต์คอนโซลแบบ native แต่ต้องนั่งปรับอยู่นานกว่า 2 ชั่วโมงกว่าจะได้ใกล้เคียงประมาณ 90%
แปลกใหม่และแฮ็ก ๆ ดี เลยสนุก จริง ๆ แล้ว เทคนิคการเรนเดอร์ 3D แทบทั้งหมดก็เป็นแบบนั้น แต่ถ้าผลลัพธ์ไม่ได้ตั้งใจจะจำลอง BBS ยุคเก่า มันก็ไม่ได้สวยงามเป็นพิเศษ
ถ้าเพิ่มบิตเข้าไปอีกก็อาจปรับปรุงได้ แต่ก่อนที่มันจะดูดีขึ้นมาก คุณคงหาวิธีที่ง่ายกว่าสำหรับกำหนดบิตทั้งหมดได้แล้ว สุดท้ายก็ย้อนกลับมาที่จุดเริ่มต้น เพราะแทบไม่มีวิธีไหนมีประสิทธิภาพกว่าการวาดเป็นพิกเซลขาวดำในโปรแกรมวาดภาพแล้วบันทึกเป็นเท็กซ์เจอร์
ถ้าอยากรู้ว่าวิธีทั่วไปกว่าที่เอนจินเรนเดอร์ 3D สมัยใหม่ใช้วาดข้อความคืออะไร ลองค้นหา SDF text และเทคนิคที่เกี่ยวข้องอย่าง MSDF ดู เป็นวิธีที่ใช้ texture atlas แบบดั้งเดิมในขั้นตอน preprocessing เพื่อสร้าง atlas ของ signed distance field
บทความนั้นพูดถึงฮาร์ดแวร์ แต่ซอฟต์แวร์ก็มีวัฏจักรเวียนว่ายเหมือนกัน
เมื่อก่อนผมเคยทำเวอร์ชันพื้นฐานมาก ๆ ของแนวคิดนี้ไว้: https://www.shadertoy.com/view/sdXBDs
ไม่ได้หมายความว่าไม่ใช่ทริกที่เจ๋งนะ มันเป็นทริกที่เจ๋งจริง ๆ
ยังมีทางเลือกในการเรนเดอร์ข้อความเป็น mesh ด้วย TextMeshPro ไปไกลกว่านั้นอีกขั้น โดยใช้ signed distance field เพื่อรองรับการสเกลได้ตามต้องการ
https://docs.unity3d.com/Packages/com.unity.textmeshpro@4.0/...
ตัวอย่าง: https://sluglibrary.com
mesh และ SDF ทำให้ฝั่ง GPU ง่ายกว่ามาก แต่ถ้าขยายใหญ่เกินไปความแม่นยำอาจลดลง และถ้าย่อ mesh เล็กเกินไปก็อาจเกิด aliasing ได้
เจ๋งมาก ถ้ามี การเปรียบเทียบประสิทธิภาพ กับวิธีใช้เท็กซ์เจอร์แบบ “ดั้งเดิม” ก็น่าสนุกดี
เวลาทำงานง่าย ๆ แบบนี้บน GPU ยุคใหม่ คำตอบของคำถามว่า “ประสิทธิภาพเป็นยังไง?” มักจะเป็น “ก็ใช้ได้”
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...
เกมนี้น่าสนใจตรงที่เก็บข้อมูลคำถามและคำตอบไว้ในเทป 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 ถอดรหัส ก็อาจได้ข้อดีทั้งสองด้าน
ถามด้วยความไม่ค่อยรู้เรื่องกราฟิกคอมพิวเตอร์สมัยใหม่เท่าไรนะครับ ค่าใช้จ่ายในการอัปโหลด texture ขนาดเล็กขึ้น GPU มันสูงขนาดนั้นเลยหรือ? สงสัยว่าทำไมไม่เรนเดอร์สตริงทั้งหมดเป็น texture 2D แล้วแสดง texture นั้นบนสามเหลี่ยมสองรูป
ในทางกลับกัน คุณต้องเขียนโค้ดสร้าง font atlas หรือหา atlas ที่มีอยู่แล้วมาโหลด และนั่นก็ต้องมีโค้ดโหลดด้วย หรือไม่ก็ต้องวาดข้อความทั้งข้อความลงใน texture แล้ว cache ผลลัพธ์ไว้จนกว่าข้อความจะเปลี่ยน
นอกจากนี้ยังต้องจัดการ resource และ binding ด้วย แต่วิธีนี้ไม่ต้องใช้ resource กล่าวอีกอย่างคือไม่ใช่โซลูชันข้อความทั่วไป แต่เป็นเทคนิคสำหรับแสดงข้อความดีบักบนหน้าจอ
อ้างอิงเพิ่มเติม เบราว์เซอร์และ OS ส่วนใหญ่ทำงานด้วยวิธีวาดข้อความลง texture โดยวาดฟอนต์แบบไดนามิกลงใน texture atlas แล้วใช้ glyph จาก atlas นั้นสร้าง texture เพิ่มสำหรับบางส่วนของหน้าต่างแอป ถ้าเปิดแสดงขอบเขต texture ในเบราว์เซอร์ คุณจะเห็น texture ทั้งหมด และ Rendering->Layer borders จะตีกรอบ texture แต่ละอันด้วยสีฟ้าอมเขียว
ถ้า CPU ที่ค่อนข้างช้าต้องเรนเดอร์กล่องข้อความแบบ texture อิสระจำนวนมาก ก็จะสะสมอย่างรวดเร็วและกินงบประสิทธิภาพ
การวาดโดยใช้ glyph atlas ยังคงดีกว่ามากในแง่การใช้ resource pipeline เรนเดอร์ข้อความสมัยใหม่มักใช้ SDF หรือเส้นโค้ง Bézier ที่เข้ารหัสไว้ เพื่อเพิ่มความอ่านง่ายของ 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 นั่นคือการแลกระหว่างความเร็วกับพื้นที่ คำตอบจะขึ้นกับว่าจะใช้หน่วยความจำกับข้อความมากแค่ไหน ข้อความเปลี่ยนหรือไม่ ต้องการเอฟเฟกต์รายตัวอักษรหรือเปล่า ฯลฯ
อีกอย่าง ถึงจะเรียกว่า “ไม่มี texture” แต่วิธีนี้ก็ยังเป็น texture อยู่ดี เพียงแต่ texture ถูกเก็บในรูปแบบอื่นและตำแหน่งอื่น การเรนเดอร์ฟอนต์แบบไม่มี texture จริง ๆ คือการประเมินเส้นโค้งเวกเตอร์แบบทันที
ทั้งที่บอกว่าจะไม่เก็บบิตแมปไว้ในเชดเดอร์ แต่กลับอธิบายวิธี เก็บบิตแมปไว้ในเชดเดอร์ อย่างชัดเจน เลยค่อนข้างสับสน
สรุปก็คือเป็นการฝังฟอนต์บิตแมปไว้ในเชดเดอร์
เปรียบได้กับความต่างระหว่างการเก็บข้อมูลไว้ในไฟล์แยกที่ต้องอ่านตอนรันไทม์ กับการใส่ข้อมูลไว้ในซอร์สโค้ดโดยตรง