ปรับภาพเกมยังไงดี: Anti-Aliasing

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

บทความชุดนี้ประกอบด้วย

  • Anti-Aliasing
  • VSync
  • FOV
  • อื่นๆ ที่ไม่ยาวพอ

Anti-Aliasing

ปกติภาษาไทยเราก็จะเรียกมันว่าลบรอยหยัก ก่อนอื่นอธิบายถึงทฤษฎีก่อนแล้วกันครับว่าทำไมเกมถึงมีรอยหยักตามขอบภาพ

สมมุติว่ามีเส้นตรงเส้นนึงที่เราจะแสดงผลบนจอ ทีนี้บนจอภาพเราน่าจะทราบดีว่าประกอบด้วยจุดพิกเซลต่างๆ

ถ้าเส้นนั้นมันตัดไม่เต็มจุดพิกเซลล่ะครับ… จะแสดงผลยังไง

Screenshot from 2016-02-07 17-44-04

วิธีที่คอมพิวเตอร์ใช้ตัดสินใจเรียกว่า Sampling ครับ วิธีการคือใน 1 pixel นั้นจะกำหนด 1 ตำแหน่งขึ้นมาเป็นตำแหน่งแทนพิกเซลนั้นๆ ถ้าจุดนั้นมีสีอะไร ก็ให้ทั้งพิกเซลนั้นมีสีนั้นไปด้วย

ก่อน
ก่อน

หลัง
หลัง

มองแบบนี้ก็เริ่มเห็นแล้วนะครับว่า เส้นมันไม่เป็นเส้นแล้ว เราจะแก้ไขปัญหาอย่างไรดี..?

SSAA

วิธีแรกง่ายมากครับ SSAA = Supersampling AA หรือบ้างก็เรียก FSAA = Full screen AA หลักการก็คือ render เกมให้ใหญ่กว่าจอของเรา แล้วย่อภาพลงมาให้พอดีจอ ทำให้ภาพที่ได้จะมีความคมชัดมาก

ภาพประกอบ: PC Gamer

(ถ้าเล่นเกมบนจอ 4k แล้วเปิด 4x SSAA นี่แปลว่าเกมมันทำงานในความละเอียด 8k อยู่นะครับ!)

MSAA

ปัญหาคือ SSAA โคตรกินเครื่องเลยครับ ก็เลยมีคนคิดค้นวิธีอื่นๆ ขึ้นมาอีก

MSAA (Multisampled AA) เป็นวิธีคล้ายๆ กับ SSAA แต่ shader จะรันในความละเอียดเท่ากับความละเอียดแสดงผล ทำให้กินเครื่องน้อยลง

ภาพประกอบ: The Danger Zone

FXAA/MLAA

Fast Approximate AA เป็นวิธีฉลาดแกมโกงครับ โดยเอาภาพที่ render เสร็จแล้วมาหาขอบต่างๆ แล้วก็ blur ทิ้ง! ง่ายมั้ยล่ะ…

ข้อเสียคือเบลอแบบนี้ก็จะทำให้ texture เบลอไปด้วย

Source: Hardforum
ภาพประกอบ: Hardforum

MFAA

ใน driver NVIDIA จะมีให้เปิด Multiframe Anti-Aliasing ครับ ซึ่งหลักการคือจะใช้จุดอ้างอิงหลายๆ จุดในเฟรมแล้วก็เฉลี่ยค่าสีกัน ปัญหาก็คือวิธีนี้มันก็คล้ายๆ SSAA ใช่มั้ยล่ะครับแล้วมันก็จะช้า

วิธีแก้ของ MFAA คือการใช้หลายๆ เฟรมเข้ามาอ้างอิงแทนครับ เฟรมแรกใช้จุดนึงอ้างอิงสี เฟรมถัดมาเปลี่ยนจุดแล้วเฉลี่ยไป ฉะนั้นก็จะทำให้ไม่กินเครื่อง

ข้อเสียของ MFAA คือถ้า framerate ไม่ถึง 40 ใช้ไม่ได้นะครับ และถ้าภาพเป็นภาพเคลื่อนไหวก็จะทำให้เกิด ghosting หรืออาการภาพค้างจางๆ

ภาพประกอบ: PCPer
ภาพประกอบ: PCPer

SMAA

Enhanced Subpixel Morphological หรือ SMAA เป็นเทคนิคใหม่จาก Crytek ครับ ปัจจุบันยังไม่เห็นเกมไหนรองรับ ถ้าจะใช้ต้องใช้ SweetFX หรือ injectSMAA

หลักการทำงานคล้ายๆ กับ FXAA คือเบลอขอบภาพ แต่วิธีการหาขอบจะใช้วิธีที่ใช้ตรวจการทำงานของ shader ใน MSAA ฉะนั้นแล้วจะทำให้ภาพมีคมชัด ไม่เบลอ texture มาก และกินเครื่องไม่มากไปกว่า FXAA เท่าไร

ภาพประกอบ: RadeonPro
ภาพประกอบ: RadeonPro

แบบไหนดี?

ปกติแล้วเกมจะมีให้เลือกปรับชนิดได้ 1-3 แบบนะครับ แล้วก็อาจจะมีตัวคูณไปอีก ตรงนี้ก็ปรับแล้วแต่ความชอบเลย แต่โดยทั่วไปแล้วนะครับ

  1. SSAA คมสุด กินเครื่องสุด
  2. MSAA
  3. SMAA ภาพดี กินเครื่องไม่เยอะ
  4. FXAA

อาจจะมีตัวอื่นๆ นอกเหนือจากนี้ อันนี้ลองหาข้อมูลเพิ่มเติมดูครับเพราะจะยกมาเฉพาะตัวที่สำคัญๆ เท่านั้น

Source

Rust first time

ว่าจะเขียนในเฟส แต่ยาวแล้ว formatting ในเฟสคนคงอ่านไม่รู้เรื่อง เลยเขียนในบล็อคละกัน

Cargo

  • งงชื่อไฟล์มาก คือตอนแรกนึกว่าตั้งชื่อไฟล์มั่วๆ ได้ ปรากฏว่าไม่ใช่ มันมีชื่อไฟล์บังคับ src/main.rs src/lib.rs เข้าใจว่ามาจาก cargo เลยไม่อยู่ในเอกสารของ rust – -!!
  • งง import ต่อ คือถ้าใช้ library ภายนอก ต้องประกาศ extern crate ที่ไฟล์ main ไม่ใช่ไฟล์ที่จะใช้ ในไฟล์ที่จะใช้ให้ใช้คำสั่ง use อย่างเดียว
  • นั่นแปลว่าถ้าใช้ในไฟล์ main ต้องย้ำ extern crate ..; use ..; ฮ่วย
  • เป็น package manager ที่เป็น build tool ด้วย เราว่าโอเคดี ยกเว้นแต่ว่ามันระบุ option rustc ตรงๆ ไม่ได้ (ว่าจะลอง -C target-cpu=native)
  • เขียน C แล้วเอา rust เรียก C ง่ายมาก ตัว cargo มันจะ compile C ให้ในขั้นตอน build เลยด้วย ไม่ต้องทำแยกกัน
    • ปัญหาอย่างเดียวคือ C String เป็น 0 terminate แต่ rust string เป็น vector (เข้าใจว่า C++ ก็ใช้ vector เป็น string?) เวลาแปลงข้ามไปมาก็สนุกเลย

Data type

  • คอมไพล์ไม่ผ่านบ่อยมาก ครึ่งวันน่าจะหมดไปกับแก้ compile error
  • คอมไพล์เสร็จตามที่เค้าโม้ครับคือคุณไม่มีทาง segmentation fault ได้จริงๆ
  • การ proof ให้ rust เชื่อว่า memory safe มหาโหดมาก
    • ในภาษาปกติ let a = b; นี่แปลว่า a มีค่าเหมือนกับ b แต่ใน rust มันแปลว่าเลิกใช้ b ได้เลย เพราะ a เป็นเจ้าของค่าใน b แล้ว และค่าหนึ่งมีเจ้าของคนเดียว
    • เดี๋ยวมีต่อด้านล่างว่ามันทำให้ optimize ไม่ได้ด้วย
  • ที่ตลกมาก คืออ่าน struct จากไฟล์ทำไม่ได้… เหตุผล เพราะคุณแน่ใจได้ยังไงว่าอ่านมาแล้ว struct ในไฟล์มันตรงกับ memory layout คุณจริง
    • ผลคือ ไปเขียนตัวอ่าน struct ใน C มาแล้วคอมไพล์เข้ามาเป็น rust เขียนง่ายกว่า ไม่งั้นคุณต้องมานั่ง proof ให้ rust เชื่อเรื่อง data type มหาศาล
  • Type inference ของ rust โคตรเก่ง ยอมรับเลย คือนอกจากตรงที่ภาษาบังคับแทบจะไม่ต้องระบุ data type เลย
  • Compiler บังคับ coding standard ด้วย คุณจะต้องใช้ under_score_function_name ทั้งหมด ไม่งั้น warning
  • rust string เป็น utf8 เท่านั้น ถ้าคุณจะใช้ encoding อื่นกรุณาเก็บใน [u8] (array ของ nt8_t)

  • ถ้าต้องการจะอ่าน uint_32 ในไฟล์ แต่ read ของ rust มันอ่านเป็น [u8]; จะแปลงยังไง
    • คำตอบคือ let id: u32 = unsafe{::std::mem::transmute_copy(& buffer);}
    • transmute_copy คือบอกว่า pointer นั้นอะจริงๆ แล้วมันชี้ไปหา data type นี้นะ คือไม่มีการ cast อะไรทั้งสิ้น เปลี่ยนวิธีมองตำแหน่งใน memory เลย แต่ถึงจะบอกว่า unsafe มันก็ยังเช็คให้ว่า memory ตรงนั้น กับ data type ใหม่มีขนาดเท่ากัน
  • rust ไม่มี null รู้สึกภาษาใหม่ๆ จะมาแนวนี้หมด ไม่ว่าจะ Kotlin หรือ swift คือจะใช้ระบบ Optional แทน
    • ไอเดียคือฟังก์ชั่นต้องประกาศว่าฟังก์ชั่นนี้ return เป็น optional แล้วคนที่เรียกก็ต้องไปเช็คเสมอว่า return มามีค่าหรือเปล่า
    • ข้อดีคือไม่เกิด null pointer แน่ๆ เพราะบังคับให้คุณเขียนจัดการ null
    • ข้อเสีย if ทุก method call เลยมั้ยครับท่าน…
  • Rust ไม่มี Class แต่ struct มี method ได้เรียกว่า implementation (สุดท้ายคือหน้าตาออกมาโคตรเหมือน class แค่ไม่มี inheritance)
  • ไม่ใส่ ; ก็ได้ แต่การใส่ ; มีผล!!!! ผมนี่สตั้นเลย ลองดูโค้ดนี้นะครับ
fn x() -> u8 {
   y();
}
fn x() -> u8 {
   y()
}
  • จะบอกว่าโค้ดแรก คอมไพล์ไม่ผ่าน ครับ เพราะมันถือว่ามี 2 statement คือ y() กับ statement เปล่า มันเลย return เป็นค่าว่างเปล่า (()) ไป!!! ในขณะที่อันหลังมีแค่ statement เดียว ก็จะ return คำตอบของ y() (ตรงนี้คล้ายๆ ruby ที่ expression สุดท้ายของฟังก์ชั่นเป็น return value ของฟังก์ชั่นด้วย)

บ่น

ผมจะทำ object pool ก็เขียนมา

let record = Record{
    id: 0,
    text: ....,
};

อ่าว… แล้ว text เป็น String จะใส่ว่าอะไร… เอางี้ละกัน

let empty_string = String::new();
let mut record = Record{
   id: 0,
   text: empty_string,
};
loop{
   reader.read_recycle(&mut record);
   let text = record.text;
   record.text = empty_string;
   put_ds(text);
}

ปรากฏว่า empty_string อันหลังนั่นมันถือว่าบรรทัดที่ 3 ยึดเป็นเจ้าของไปแล้ว ใช้ไม่ได้ สุดท้ายก็เลยต้อง String::new() สองรอบ นี่มันก็ไม่ต่างกับไม่ทำ object pool แล้วนะ!!

Speed

  • สรุปแล้วถามว่า rust เร็วมั้ย คำตอบคือไม่ครับ มันเป็นห่วง memory safety เกินไป คือบางอันคุณเห็นกับตาว่าคุณไม่ใช้ แต่คุณพิสูจน์เป็นโค้ดให้ rust ไม่ได้คุณก็เขียนไม่ได้ แล้วมันทำให้ต้องมานั่งอ้อมโลกจนน่ารำคาญ
  • แต่อย่าลืมนะครับว่ามันเป็นภาษา low level ฉะนั้นแล้วมันแค่ช้ากว่า C แค่นั้นแหละครับ….