วิธีแก้ปัญหาสระลอยแบบระยะยาว

วันนี้จะคุยกันเล็กน้อยเกี่ยวกับเรื่องการวาดตัวหนังสือครับ เรื่องนี้เป็นเรื่องที่ถ้าเป็นสักสิบห้าปี หรือยี่สิบปีก่อนเนี่ย เป็นเรื่องที่คุยกันเยอะมาก เพราะแอพส่วนใหญ่ในขณะนั้นวาดตัวหนังสือภาษาไทยแล้วมักจะมีปัญหาครับ ที่เห็นบ่อย ๆ คือปัญหาที่เรียกว่า “ปัญหาสระลอย”

ปัญหาสระลอย

ปัญหาสระลอย คือการที่ สระระดับบน (หรือที่เรียกว่าสระลอย) มันไปบังบรรณยุกต์ ทำให้ไม่สามารถแยกแยะบางคำได้ (เช่น ที กับ ที่) ทางแก้ในขณะนั้นก็คือการยกวรรณยุกต์ให้สูงขึ้นไปอีก ซึ่งจะทำให้สระไม่บังวรรณยุกต์ แต่ในกรณีที่ไม่มีสระระดับบน วรรณยุกต์จะลอยเท้งเต้งอยู่บนฟ้าแทน

(ปัญหานี้ผมชอบเรียกว่า ปัญหาวรรณยุกต์จม มากกว่า แต่คำว่าสระลอยเป็นที่รู้จักกันมากกว่า)

เนื่องจากพัฒนาการของระบบการวาดตัวหนังสือ ทำให้เราแทบจะไม่ค่อยเจอปัญหานี้แล้วในปัจจุบัน ระบบปฎิบัติการณ์และ web browser ที่ระบบรองรับการวาดตัวหนังสือที่ซับซ้อน (โดยการใช้ฟอนต์แบบ OpenType) ทำให้การวาดตัวหนังสือนั้นทำได้ถูกต้อง

แต่จากการที่ผมสำรวจ Game Engine ในตลาดส่วนใหญ่นั้นยังคงมีปัญหานี้อยู่ (รวมทั้งตัวที่นิยมกันในตลาดอย่าง Unity3d และ Unreal Engine)

ที่มาของปัญหานี้คือ ในระบบ TrueType ที่นิยมกันในอดีตนั้น Glyph (ภาพตัวอักษร) แต่ละตัวจะมีตำแหน่งตายตัว โดยอ้างอิงจากจุดเริ่มต้นและเส้นบรรทัด (baseline) การวางตำแหน่งจะไม่มีความเกี่ยวโยงกับตัวอักษรอื่น ๆ ที่อยู่รอบ ๆ เช่น ตำแหน่งของวรรณยุกต์จะไม่สัมพันธ์กับสระหรือพยัญชนะ

(ภาพจาก Freetype.org – ลองเข้าไปอ่านสำหรับข้อมูลเพิ่มเติมนะครับ )

และ Game Engine ทุกตัวที่ผมค้นคว้ามานั้น ก็ใช้วิธีการวางตำแหน่งแบบเดียวกับ TrueType ก็เลยวางตำแหน่งผิดเหมือนกัน

วิธีการแก้ไขปัญหาสระลอยแบบทั่วๆ ไป

เท่าที่ผมสังเกต การแก้ปัญหาสระลอยที่เป็นที่นิยมในหมู่นักพัฒนาเกมไทย มักจะใช้วิธีที่เรียกว่า Glyph Substitution (GSUB) หรือการแทนที่ภาพตัวอักษร วิธีที่จะใช้วิธีการตรวจสอบว่า ตัวหนังสือที่อยู่ก่อนหน้าวรรณยุกต์นั้นเป็นตัวไหน แล้วทำการเลือกว่าจะใช้วรรณยุกต์ตัวไหน

  • ถ้าข้างหน้าวรรณยุกต์เป็นสระระดับบน ใช้วรรณยุกต์ที่อยู่สูงเพื่อหลบสระ
  • ถ้าข้างหน้าวรรณยุกต์เป็นพยัญชนะ ใช้วรรณยุกต์ที่อยู่ระดับต่ำ

วิธีนี้เป็นวิธีที่ใช้ในอดีตเหมือนกัน (ก่อนที่ OpenType จะแพร่หลาย) แต่ก็มีข้อเสียตรงที่การวางตำแหน่งจะยังตายตัวอยู่ อย่างการเขียนคำว่า ปี่ นั้นอาจจะได้ผลที่ไม่ถูกต้อง เพราะว่าต่อให้ปรับระดับความสูงต่ำตามกฎข้างบน หางของตัว ป ก็ยังคงไปปิดสระหรือพยัญชนะนั้นอยู่ดี

การที่จะวางวรรณยุกต์ (และสระระดับบน) ให้ถูกต้องด้วยกฎตายตัวนั้นทำให้สวยได้ยาก เพราะมีกรณีจำนวนมากที่จะต้องจัดการในโค๊ด รวมทั้งจำนวน Glyph และ Glyph Metrics ที่จะต้องโหลดเข้าไปอีก

วิธีการแก้ไขปัญหาสระลอยแบบ OpenType

OpenType เป็น super set ของ TrueType โดยมีการเพิ่มฟีเจอร์ต่าง ๆ เข้าไปในมาตรฐานเดิม สำหรับภาษาไทยก็มีฟีเจอร์ที่สามารถนำมาใช้เพื่อทำให้สามารถวาดตัวหนังสือได้ถูกต้อง อย่างการแทนภาพตัวอักษร หรือการวางตำแหน่งภาพตัวอักษรใหม่ (Glyph Positioning — GPOS) Microsoft ได้แนะนำว่า สำหรับฟอนต์ภาษาไทยควรมีฟีเจอร์ดังต่อไปนี้

ภาพจาก Microsoft — สำหรับรายละเอียดของฟีเจอร์แต่ละตัวนั้นสามารถดูได้จากเว็บไซท์ของ Microsoft นะครับ

ทีนี้ถ้าเราดูในตารางจะเห็นว่า มีแค่ฟีเจอร์สองตัวสุดท้ายเท่านั้นที่ถูกระบุว่า “Required” เท่านั้น ฟีเจอร์ด้านบนไม่ได้สำคัญมาก กล่าวคือ มันเป็นฟีเจอร์ที่ทำให้เรนเดอร์ประโยคออกมาได้สวยงาม แต่ไม่ได้จำเป็นสักเท่าไหร่ อย่าง ccmp เนี่ย ที่เห็นชัด ๆ คือ คำว่า “กตัญญู” ตัว ญ + ู นั้นตามหลักแล้วเราไม่จำเป็นจะต้องเขียนเชิงของตัว ญ ซึ่ง ccmp ของภาษาไทยมันจะมีกฎข้อนี้ระบุอยู่ แต่ถ้าเขียนไว้มันก็ไม่ได้เสียหายครับ ตราบใดที่ไม่ได้เขียนทับกัน

หรือ kerning ภาษาไทยเนี่ยแทบไม่ได้จะจำเป็นเลย

ดังนั้น ถ้าเราจะนำวิธีของ OpenType มาใช้เนี่ย เราทำแค่ mark-to-base (mark) กับ mark-to-mark (mkmk) ก็พอแล้วครับ

Mark กับ Base

การวางตำแหน่งแบบ mark-to-mark กับ mark-to-base นั้นจริง ๆ คล้ายกันมาก ๆ คือ มันจะเป็นการจับคู่กันระหว่างตัวอักษรสองตัว ตัวอักษรแต่ละตัวจะมีตำแหน่งตะขอ (anchor) อยู่ เราก็แค่เอาตะขอของตัวอักษรตัวหลังมาเกี่ยวกับตัวหน้าซะ ก็คือ เราจะเลื่อนตำแหน่งของภาพตัวอักษรที่อยู่ข้างหลังมาจนกว่าตำแหน่งตะขอของตัวหลังนั้นไปทับกับตัวหน้าครับ

ยกตัวอย่าง เช่น คำว่า ป่า เนี่ย เราก็จะวางแบบนี้

ทีนี้ mark กับ base แตกต่างกันยังไง ? โดยทั่วไป mark จะเป็นเครื่องหมายที่ลอยอยู่ด้านบน หรือลอยอยู่ด้านใต้ของ base ครับ ส่วน base ก็มักจะเป็นพวกตัวอักษรที่อยู่บนเส้นบรรทัด (baseline) อีกอย่างหนึ่งก็คือ ตัวอักษรที่เป็น mark จะอยู่ข้างหลัง base เสมอครับ โดยตัวอักษร base ที่ mark จะไปเกาะอยู่นั้นไม่จำเป็นต้องอยู่ติดกันก็ได้ แค่เป็นตัวอักษรตัวสุดท้ายที่อยู่หน้า mark ก็ถือว่าเป็นคู่ที่เกาะกันครับ เช่นคำว่า “ปู่” เป็นต้น คำนี้ประกอบด้วยตัวอักษรสามตัว คือ ป ู และ ่ ซึ่งทั้ง ูและ ่ นั้นก็เกาะอยู่กับ ป ทั้งนั้น แค่อยู่กันคนละจุด

จากตัวอย่างข้างบนก็น่าจะเห็นคุณสมบัติอีกอย่างหนึ่งของ base ก็คือ มันมีตะขอได้หลายจุดนั้นเอง และที่ mark เองก็ต้องระบุได้ว่าจะเกาะกับตะขอไหนของ base ด้วย

Mark กับ Mark

mkmk จะเป็นการจับคู่ตัวอักษรที่เป็น mark ด้วยกัน อย่างเช่นคำว่า ปี่ เป็นต้น คำนี้จะใช้ mark-to-base ตรงคู่ของ ป กับ ี แล้วคู่หลังจะเป็น mark to mark ครับ

ความแตกต่างระหว่าง mark-to-mark กับ mark-to-mark นั้นก็คือ mkmk จะเป็นจับคู่ระหว่าง mark ที่อยู่ติดกันเท่านั้น (mark-to-base จะจับระหว่าง mark กับ base ที่ใกล้ที่สุดแต่ไม่จำเป็นต้องติดกัน) และ mark-to-mark นั้น ตัวอักษรตัวหน้าจะมี anchor แค่จุดเดียวเท่านั้น

การ implement

ข่าวไม่ดีอันแรกคือ ผมยังไม่เคยเห็นใครสร้าง font สำหรับเกมที่มีฟีเจอร์การปรับตำแหน่งด้วยตะขอแบบนี้เล่าให้ฟังมาข้างบนเลยครับ (อาจจะมีเป็น plugin ของ Unity3d แต่ผมไม่เคยลองนะ) ดังนั้น เราอาจจะต้องสร้างเอง หรือ … ไปบีบให้ผู้สร้าง Engine เขาทำมาใส่ Engine ให้หน่อย

สำหรับคนที่เลือกที่จะทำเอง วิธีที่ง่ายที่สุดก็คือการสร้างฟอนต์จากฟอนต์แบบ OpenType ที่มีอยู่แล้ว แต่การอ่านไฟล์ OTF/TTF นั้นออกจะเป็นเรื่องที่โหดร้ายไปหน่อยนึง (ตัวไฟล์มีลักษณะเป็น BigEndian Binary ประกอบด้วยตารางย่อย ๆ มากมาย และอ่านลำบากพอสมควร) ผมพบว่ามี Utility บางตัวที่สามารถแตกข้อมูลไบนารีออกมาให้อยู่ในรูปที่อ่านได้ง่ายขึ้น เช่น OTFCC ที่สามารถดัมป์ไฟล์ ttf/otf ให้เป็น json ได้

จากไฟล์ json ผมเขียน tool สำหรับ สร้าง glyph texture และ font file ที่เป็นฟอร์แมทส่วนตัว ข้างในมี กฎการทำ glyph positioning และอื่น ๆ เอาไว้ แล้วก็สร้างตัวอ่านไฟล์ + ตัววาดตัวอักษรในเอนจินของตัวเอง

ข้างล่างนี้เป็นสกรีนชอตจากโปรเจคตัวอย่างที่ผมทำอยู่ครับ (ฟอนต์ที่ใช้เป็น Noto Sans UI Thai ตัวอัลฟ่าครับ) ตัวสีขาวเป็นตัวที่มีการทำการวาง layout ใหม่ ส่วนตัวสีเทาคือตัวที่ใช้วิธีแบบ TrueType เหมือนเอนจินทั่ว ๆ ไป

เรื่อง performance impact ก็มีบ้างพอสมควรครับ เพราะต้องทำลูปหลายรอบอยู่กว่าจะคำนวนหมด ทริคคือพยายามคำนวน layout แค่ครั้งเดียวแล้วใช้ไปเรื่อย ๆ อย่าคำนวนใหม่บ่อย ๆ

AlDrums – โปรแกรมตีกลองกับ OpenAL

โปรแกรมนี้เป็นโปรแกรมตีกลอง เขียนด้วย SDL เป็นตัวจัดการด้านกราฟิคและส่วนประกอบอื่น ๆ (เนื่องจากผมขี้เกียจเปิดหนังสือ Win32 นั่นเอง) และส่วนที่เกี่ยวกับเสียงทั้งหมดจะเป็น OpenAL ครับ ผมเขียนขึ้นมาเป็น Concept Prove ดูว่าถ้าทำขึ้นมาจริง ๆ มันจะเล่นได้มั้ย

OpenAL ก็คล้าย ๆ กับ OpenGL แต่เป็นเรื่องเสียงครับ ประวัติศาสตร์ของมันเป็นไงก็ไปหาอ่านเองนะ (ฮา) ที่แน่ๆ คือวิธีใช้มันใกล้เคียงกับ OpenGL มาก ๆ แต่มีเรื่อง Device กับ Device Context เพิ่มขึ้นมา OpenAL เป็นระบบเสียงสามมิติโดยกำเนิด ดังนั้นโปรแกรมนี้ก็จะเป็นระเบบเสียงสามมิติเช่นกันครับ จะฟังออกเลยว่าเสียงไหนอยู่ตรงไหน ถ้าใช้หูฟังดี ๆ นะ

ผมเพิ่มความน่าสนใจเข้าไปอีกหน่อยโดยการใส่เอฟเฟค Reverb เข้าไป ซึ่งตัว OpenAL Effect Extension นั้นจะเป็นรูปแบบ Send/Return เหมือนกับพวกโปรแกรมทำเพลงอยู่แล้ว และ Reverb ตัวนี้เสียงเข้าท่าทีเดียว (ขอเตือนก่อนว่า ท่านใดที่เครื่องไม่รองรับ Effect Extension จะรันไม่ขึ้นนะครับ … คือผมขี้เกียจเช็คเงื่อนไขน่ะ 555 แหมเอาแค่ทดลองเฉย ๆ เอง)

โปรแกรมนี้ทำงานง่าย ๆ ก็คือ พอมี Event กดคีย์บอร์ดปุ๊บ ก็ไปเล่นเสียงปั๊บ ง่ายมาก ๆ ใช่มั้ยครับ โดยโปรแกรมนี้ปัจจุบันจะใช้ทั้งหมด 8 เสียง คือ เบสดรัม แสนร์ดรัม ทอมทอม ฟลอร์ทอม ไฮแฮทปิด-เปิด ไรด์ แล้วก็ แครช ครับ อิอิ (ขี้เกียจเปลี่ยนภาษาอีกแล้ว) มีจุดสำคัญตรงที่ ไฮแฮทปิดกับเปิดนั้น ต้องไม่เล่นพร้อม ๆ กัน ต้องหยุดเสียงใดเสียงนึงก่อนที่จะเล่นอีกเสียง ไม่งั้นมันจะฟังประหลาดครับ

หน้าตามันตอนนี้ไม่มีอะไรเลย (เนื่องจากขี้เกียจอีกแล้ว) เป็นหน้าต่างดำ ๆ ก็ปล่อยมันไว้งั้นแหละ

ปุ่มที่ใช้มีดังนี้ครับ
Bass Drum – Space Bar
Snare Drum – f หรือ j
Closed Hi-Hat – g หรือ h
Opened Hi-Hat – t หรือ y
Tom Tom – d หรือ k
Floor Tom – s หรือ l
Ride Cym – r หรือ u
Crash Cym – e หรือ i

ลองเล่น ๆ ดูนะครับ ก่อนจะเล่นคุณจะต้องลง OpenAL Runtime ก่อนนะครับ แล้วก็ต้องมี Microsoft Visual C++ 2008 Redistributable Package (เพราะผมเขียนด้วย VC 2008 ครับ) สำหรับคนที่เล่นเกม ส่วนใหญ่น่าจะมีทั้งสองตัวนี้อยู่แล้วครับ (มันมากับหลายเกมมาก ๆ ) ส่วนอีกสองไฟล์ที่ต้องใช้ (SDL.dll กับ alut.dll) ผมติดมาให้แล้ว เผื่อว่าไม่มีครับ

โหลดได้ที่นี่ครับ AlDrums ต้องบอกก่อนว่าจะมีหลายๆ กรณีที่จะเปิดไม่ติด หรือเปิดแล้วเห็นเป็นดำ ๆ แล้วปิดไปเลย หรือเงียบไปเลย (และอื่น ๆ ) ก็ไม่ต้องตกใจนะครับ เครื่องคุณยังไม่พัง พอดีว่าโปรแกรมยังไม่ได้เพิ่มส่วนแจ้งให้ผู้ใช้ทราบปัญหาน่ะครับ พอเจออะไรก็จะพาลปิดไปเลยครับ ไม่ต้องซีเรียสนะ

สิ่งที่ผมทดลองตรงนี้ มีดังนี้ครับ

  • ทดสอบว่า การวางปุ่มแบบนี้มันเล่นถนัดมั้ย ?
  • เช็คเรื่อง Latency ของ OpenAL ว่ามันเยอะขนาดเล่นไม่ได้เลยหรือไม่
  • ระบบเสียง 3มิติ และ เอฟเฟค reverb ว่าเป็นยังไง

ซึ่งกับเครื่องผมนี่ ให้ผลที่น่าพอใจมาก ๆ ครับ สำหรับท่านที่โหลดไปลอง จะให้คอมเม้นท์ในสามเรื่องนี้ด้วยก็ดีนะครับ 😀

ปล. อันนี้ไม่มีโค๊ดให้ อย่าเอาไปเลย ยังเละอยู่ …