การวาดกระเบื้องจำนวนมากด้วย OpenGL วิธีการที่ทันสมัย


35

ฉันกำลังทำงานกับเกมพีซีที่มีไทล์ / สไปรต์กับทีมงานของผู้คนและเรากำลังประสบกับปัญหาด้านประสิทธิภาพ ครั้งสุดท้ายที่ฉันใช้ OpenGL คือประมาณปี 2004 ดังนั้นฉันจึงสอนตัวเองถึงการใช้โปรไฟล์หลักและฉันรู้สึกสับสนเล็กน้อย

ฉันต้องวาดในบริเวณใกล้เคียงของกระเบื้อง 250-750 48x48 ไปที่หน้าจอทุกเฟรมรวมทั้งอาจจะประมาณ 50 สไปรต์ ไทล์เปลี่ยนเฉพาะเมื่อโหลดระดับใหม่และสไปรต์กำลังเปลี่ยนแปลงอยู่ตลอดเวลา กระเบื้องบางชิ้นประกอบไปด้วยชิ้นส่วน 24x24 สี่ชิ้นและส่วนใหญ่ (แต่ไม่ทั้งหมด) ของสไปรต์มีขนาดเท่ากับกระเบื้อง กระเบื้องและสไปรท์จำนวนมากใช้การผสมอัลฟา

ตอนนี้ฉันกำลังทำทั้งหมดนี้ในโหมดเร่งด่วนซึ่งฉันรู้ว่าเป็นความคิดที่ไม่ดี เหมือนกันเมื่อสมาชิกในทีมของเราพยายามเรียกใช้เขาจะได้รับอัตราเฟรมที่ไม่ดีมาก (~ 20-30 fps) และมันแย่กว่ามากเมื่อมีไทล์มากขึ้นโดยเฉพาะอย่างยิ่งเมื่อไทล์เหล่านั้นเป็นแบบนั้น ถูกตัดเป็นชิ้น ๆ ทั้งหมดนี้ทำให้ฉันคิดว่าปัญหาคือจำนวนการเรียกสายที่กำลังทำอยู่

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

กระเบื้อง:

  1. เมื่อมีการโหลดเลเวลให้วาดไทล์ทั้งหมดลงในบัฟเฟอร์เฟรมที่แนบกับพื้นผิวที่มีขนาดใหญ่และเพียงแค่วาดสี่เหลี่ยมขนาดใหญ่ที่มีพื้นผิวนั้นติดอยู่ทุกเฟรม
  2. ใส่กระเบื้องทั้งหมดลงในบัฟเฟอร์จุดสุดยอดแบบคงที่เมื่อโหลดระดับและวาดพวกเขาในแบบนั้น ฉันไม่รู้ว่ามีวิธีการวาดวัตถุที่มีพื้นผิวแตกต่างกันด้วยการเรียกใช้ glDrawElements เพียงครั้งเดียวหรือไม่หรือถ้านี่คือสิ่งที่ฉันต้องการจะทำ อาจจะแค่ใส่กระเบื้องทั้งหมดลงในพื้นผิวยักษ์ใหญ่และใช้พิกัดพื้นผิวตลกใน VBO

sprites:

  1. วาดเทพดาแต่ละอันด้วยการเรียกแยกต่างหากไปยัง glDrawElements ดูเหมือนว่าจะเกี่ยวข้องกับการสลับพื้นผิวมากมายซึ่งฉันบอกว่าไม่ดี อาเรย์พื้นผิวอาจมีประโยชน์ที่นี่?
  2. ใช้ VBO แบบไดนามิกอย่างใด คำถามพื้นผิวเช่นเดียวกับหมายเลข 2 ด้านบน
  3. ชี้สไปรต์? นี่อาจจะโง่

มีความคิดใด ๆ เหล่านี้สมเหตุสมผลหรือไม่ มีการนำไปปฏิบัติที่ดีที่ไหนสักแห่งที่ฉันสามารถดูได้หรือไม่?


หากไทล์ไม่เคลื่อนไหวหรือเปลี่ยนแปลงและพวกเขาดูเหมือนกันทั้งระดับคุณควรใช้ความคิดแรก - บัฟเฟอร์เฟรม มันจะมีประสิทธิภาพมากที่สุด
zacharmarz

ลองใช้แผนที่พื้นผิวเพื่อที่คุณจะได้ไม่ต้องสลับพื้นผิว แต่ทำให้ทุกอย่างเหมือนเดิม ตอนนี้เป็นอย่างไร framerate ของพวกเขา?
user253751

คำตอบ:


25

วิธีที่เร็วที่สุดในการเรนเดอร์ไทล์คือการบรรจุข้อมูลจุดสุดยอดลงใน VBO แบบคงที่พร้อมดัชนี (ตามที่ระบุ glDrawElements) การเขียนลงในรูปภาพอื่นนั้นไม่จำเป็นเลยและจะต้องใช้หน่วยความจำมากขึ้นเท่านั้น การสลับพื้นผิวมีค่าใช้จ่ายสูงมากดังนั้นคุณอาจต้องการแพ็คไทล์ทั้งหมดลงในพื้นผิวที่เรียกว่าTexture Atlasและให้แต่ละสามเหลี่ยมใน VBO พิกัดพื้นผิวที่เหมาะสม จากนี้คุณไม่ควรมีปัญหาในการเรนเดอร์ 1,000 ถึงแม้กระทั่ง 100000 ไทล์ทั้งนี้ขึ้นอยู่กับฮาร์ดแวร์ของคุณ

ความแตกต่างเพียงอย่างเดียวระหว่างการเรนเดอร์แบบกระเบื้องและการเรนเดอร์สไปรต์อาจเป็นไปได้ว่าสไปรต์เป็นแบบไดนามิก ดังนั้นเพื่อประสิทธิภาพที่ดีที่สุด แต่ทำได้ง่ายคุณสามารถใส่พิกัดสำหรับ sprite vertices ลงในกระแสดึง VBO แต่ละเฟรมแล้ววาดด้วย glDrawElements แพ็คพื้นผิวทั้งหมดใน Atlas พื้นผิวด้วย หากสไปรต์ของคุณเคลื่อนที่ไม่ค่อยคุณอาจลองสร้าง VBO แบบไดนามิกและอัปเดตเมื่อสไปรต์เคลื่อนที่ แต่นั่นก็รวมค่า overkill ที่นี่เนื่องจากคุณต้องการให้สไปรต์บางตัว

คุณสามารถดูต้นแบบเล็ก ๆ ที่ฉันทำใน C ++ ด้วย OpenGL: Particulate

ฉันแสดงสไปรต์ 10,000 จุดที่ฉันคาดเดาโดยมีค่าเฉลี่ย fps ที่ 400 บนเครื่องปกติ (Quad Core @ 2.66GHz) เป็น CPU ที่มีฝาครอบนั่นหมายความว่าการ์ดกราฟิกสามารถแสดงผลได้มากขึ้น โปรดทราบว่าฉันไม่ได้ใช้ Texture Atlases ที่นี่เนื่องจากฉันมีเพียงพื้นผิวเดียวสำหรับอนุภาค อนุภาคถูกแสดงด้วย GL_POINTS และตัวแปลงเฉดคำนวณขนาดรูปสี่เหลี่ยมจริงแล้ว แต่ฉันคิดว่ามันมี Quad Renderer ด้วย

โอ้และใช่ถ้าคุณไม่มีสี่เหลี่ยมและใช้เฉดสีสำหรับการทำแผนที่พื้นผิว GL_POINTS ค่อนข้างโง่ ;)


สไปรต์เปลี่ยนตำแหน่งและพื้นผิวที่ใช้และส่วนใหญ่ทำเช่นนี้ทุกเฟรม นอกจากนี้สไปรท์และถูกสร้างและทำลายบ่อยมาก สิ่งเหล่านี้ที่สตรีมวาด VBO สามารถจัดการได้หรือไม่
Nic

2
โดยทั่วไปการสตรีมหมายถึง: "ส่งข้อมูลนี้ไปยังการ์ดกราฟิกและทิ้งหลังจากวาด" ดังนั้นคุณต้องส่งข้อมูลอีกครั้งในแต่ละเฟรมและนั่นไม่สำคัญว่าคุณสไปรต์จำนวนเท่าใดตำแหน่งใดที่พวกเขามีพิกัดพื้นผิวใดหรือสีใด แต่การส่งข้อมูลทั้งหมดในครั้งเดียวและปล่อยให้ GPU ประมวลผลได้เร็วกว่าโหมดทันทีแน่นอน
Marco

ทั้งหมดนี้ทำให้รู้สึก มันคุ้มค่าหรือไม่ที่จะใช้บัฟเฟอร์ดัชนีสำหรับสิ่งนี้ จุดยอดเดียวที่จะทำซ้ำคือสองมุมจากทุกสี่เหลี่ยมผืนผ้าใช่ไหม (ความเข้าใจของฉันคือว่าดัชนีจะมีความแตกต่างระหว่าง glDrawElements และ glDrawArrays ที่ถูกต้องว่า.?)
Nic

1
หากไม่มีดัชนีคุณจะไม่สามารถใช้ GL_TRIANGLES ซึ่งมักจะไม่ดีเนื่องจากวิธีการวาดนี้เป็นวิธีที่มีประสิทธิภาพดีที่สุด อีกทั้งการใช้งาน GL_QUADS นั้นไม่รองรับใน OpenGL 3.0 (ที่มา: stackoverflow.com/questions/6644099/… ) สามเหลี่ยมเป็นตาข่ายดั้งเดิมของการ์ดกราฟิกใด ๆ ดังนั้นคุณ "ใช้" 2 * 6 ไบต์เพิ่มเติมเพื่อบันทึกการประมวลผลจุดสุดยอด 2 รายการและจุดสุดยอด * 2 ไบต์ ดังนั้นโดยทั่วไปคุณสามารถพูดได้ว่าดีกว่าเสมอ
Marco

2
ลิงก์ไปที่ฝุ่นละอองตายแล้ว ... คุณช่วยจัดหาใหม่ได้มั้ย
SWdV

4

ถึงแม้จะมีจำนวนของการวาดนี้เรียกคุณไม่ควรจะเห็นชนิดของการลดลงของประสิทธิภาพการทำงานที่ - โหมดทันทีอาจจะช้า แต่ก็ไม่ได้ว่าช้า (สำหรับการอ้างอิงยวบแม้รักเก่าสามารถจัดการหลายพันโทรทันทีโหมดต่อกรอบโดยไม่ล้ม ลงไปไม่ดี)

ฉันสงสัยว่ามีบางสิ่งที่น่าสนใจเกิดขึ้นที่นี่ สิ่งแรกที่คุณต้องทำคือลงทุนเวลาในการทำโปรไฟล์โปรแกรมของคุณไม่เช่นนั้นคุณจะมีความเสี่ยงอย่างมากที่จะต้องทำการปรับโครงสร้างใหม่ตามข้อสันนิษฐานที่อาจส่งผลให้ประสิทธิภาพเพิ่มขึ้น ดังนั้นให้ดำเนินการผ่านสิ่งที่เป็นพื้นฐานเช่นเดียวกับ GLIntercept และดูว่าเวลาของคุณไปที่ไหน จากผลลัพธ์ที่คุณจะสามารถแก้ไขปัญหาด้วยข้อมูลจริงบางอย่างเกี่ยวกับปัญหาคอขวดหลักของคุณ


ฉันได้ทำการทำโปรไฟล์แล้วแม้ว่ามันจะค่อนข้างอึดอัดเพราะปัญหาเรื่องประสิทธิภาพไม่ได้เกิดขึ้นในเครื่องเดียวกับการพัฒนา ฉันสงสัยเล็กน้อยว่าปัญหาอยู่ที่อื่นเพราะปัญหาเพิ่มขึ้นอย่างแน่นอนด้วยจำนวนของกระเบื้องและกระเบื้องนั้นไม่ได้ทำอะไรนอกจากวาด
Nic

แล้วการเปลี่ยนแปลงสถานะล่ะ คุณจัดกลุ่มกระเบื้องทึบแสงตามรัฐหรือไม่?
Maximus Minimus

นั่นเป็นความเป็นไปได้ สิ่งนี้ไม่สมควรได้รับความสนใจมากขึ้นในส่วนของฉัน
Nic

2

โอเคเนื่องจากคำตอบสุดท้ายของฉันออกมาจากมือนี่เป็นคำตอบใหม่ที่อาจมีประโยชน์มากกว่า


เกี่ยวกับประสิทธิภาพ 2 มิติ

คำแนะนำทั่วไปประการแรก: 2D ไม่ได้เรียกร้องให้ใช้ฮาร์ดแวร์ปัจจุบันแม้ว่ารหัสที่ไม่ได้เพิ่มประสิทธิภาพส่วนใหญ่จะทำงาน นั่นไม่ได้หมายความว่าคุณควรใช้ Intermediate Mode แต่อย่างน้อยต้องแน่ใจว่าคุณไม่ได้เปลี่ยนสถานะเมื่อไม่จำเป็น (เช่นอย่าผูกพื้นผิวใหม่ด้วย glBindTexture เมื่อพื้นผิวเดียวกันถูกผูกไว้แล้วถ้าตรวจสอบ CPU เป็นตัน เร็วกว่า glBindTexture-call) และไม่ใช้สิ่งที่ผิดอย่างสิ้นเชิงและโง่เหมือน glVertex (แม้แต่ glDrawArrays จะเร็วขึ้นและไม่ยากต่อการใช้งานมากนัก แต่ก็ไม่ได้ทันสมัยมากนัก) ด้วยกฎสองข้อที่ง่ายมากเวลาเฟรมควรน้อยกว่า 10ms (100 fps) ทีนี้เพื่อให้ได้ความเร็วมากขึ้นขั้นตอนต่อไปคือการทำแบทช์เช่นการรวมกันเป็นจำนวนมากเรียกการดึงสำหรับหนึ่งนี้คุณควรพิจารณาการใช้ atlases texture เพื่อให้คุณสามารถลดจำนวนการโยงเท็กซ์เจอร์และเพิ่มจำนวนสี่เหลี่ยมที่คุณสามารถวาดได้ด้วยการโทรเพียงครั้งเดียวเป็นจำนวนมาก หากคุณไม่ได้ลงไปประมาณ 2ms (500fps) คุณกำลังทำอะไรผิดไป :)


แผนที่ย่อย

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

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


สไปรท์

สไปรท์ต้องการความยืดหยุ่นสูงทำให้ยากมากที่จะปรับให้เหมาะสมนอกเหนือจากที่กล่าวไว้ในส่วน "เกี่ยวกับประสิทธิภาพ 2D" และถ้าคุณไม่ต้องการสไปรต์หมื่นหน้าจอในเวลาเดียวกันก็อาจไม่คุ้มค่ากับความพยายาม


1
และแม้ว่าคุณจะมีหลายพันสิบของสไปรท์ฮาร์ดแวร์ที่ทันสมัยควรจะทำงานได้ที่ความเร็วที่เหมาะสม :)
มาร์โก

@ API-Beast รออะไรนะ? คุณจะคำนวณ Texture UV ในส่วนของ shader ได้อย่างไร คุณไม่ควรส่งรังสียูวีไปยังส่วนที่แตกออก?
HgMerk

0

หากทั้งหมดอื่นล้มเหลว ...

ตั้งค่าวิธีการวาดภาพฟลอพ อัปเดตสไปรต์อื่น ๆ ในแต่ละครั้งเท่านั้น แม้ว่าจะใช้ VisualBasic6 และวิธี bit-blit ง่าย ๆ แต่คุณสามารถวาด sprites ต่อเฟรมได้ บางทีคุณควรมองไปที่วิธีการเหล่านี้เนื่องจากวิธีการวาดภาพสไปรต์โดยตรงของคุณดูเหมือนจะล้มเหลว (ฟังดูเหมือนคุณกำลังใช้ "วิธีการเรนเดอร์" มากกว่า แต่ลองใช้มันเหมือนกับ "วิธีการเล่นเกม" การเรนเดอร์นั้นเกี่ยวกับความคมชัดไม่ใช่เรื่องความเร็ว)

โอกาสที่คุณจะวาดหน้าจอทั้งหมดซ้ำแล้วซ้ำอีก แทนที่จะแค่วาดใหม่เฉพาะพื้นที่ที่มีการเปลี่ยนแปลง นั่นคือค่าใช้จ่ายจำนวนมาก แนวคิดเรียบง่าย แต่ไม่เข้าใจง่าย

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

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

สุดท้ายนี้ฉันขอแนะนำให้ จำกัด สิ่งที่คุณผสมระหว่างอัลฟากับสิ่งที่ต้องการ หากทุกอย่างต้องการมัน ... คุณไม่มีทางเลือกนอกจากต้องการผู้ใช้ของคุณมีข้อกำหนดด้านฮาร์ดแวร์ที่ดีกว่าหรือเพื่อลดประสิทธิภาพของเกมเพื่อประสิทธิภาพที่ต้องการ


-1

สร้างแผ่นสไปรต์สำหรับวัตถุและชุดไพ่สำหรับภูมิประเทศเช่นเดียวกับที่คุณทำในเกม 2D อื่น ๆ ไม่จำเป็นต้องสลับพื้นผิว

กระเบื้องที่เรนเดอร์อาจมีความเจ็บปวดได้เนื่องจากสามเหลี่ยมแต่ละคู่ต้องการพิกัดพื้นผิวของตัวเอง มีวิธีการแก้ปัญหานี้อย่างไรก็ตามเป็นก็เรียกว่าการแสดงผล instanced

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


-1: การอินสแตนซ์เป็นความคิดที่เลวร้ายยิ่งกว่าวิธีแก้ปัญหาที่สมบูรณ์แบบของ Mr. Beast อินสแตนซ์ทำงานได้ดีที่สุดสำหรับประสิทธิภาพเมื่อแสดงวัตถุที่มีความซับซ้อนปานกลาง (~ 100 สามเหลี่ยมหรือมากกว่านั้น) ไทล์สามเหลี่ยมแต่ละอันที่ต้องการพิกัดพื้นผิวไม่ใช่ปัญหา คุณเพียงแค่สร้างตาข่ายที่มีกลุ่มควอเดอร์แบบหลวม ๆ
Nicol Bolas

1
@NicolBolas เอาล่ะฉันจะทิ้งคำตอบไว้เพราะการเรียนรู้
dreta

1
เพื่อความชัดเจน Nicol Bolas คำแนะนำของคุณเกี่ยวกับวิธีจัดการกับสิ่งเหล่านี้คืออะไร? กระแสดึงของมาร์โก? มีที่ไหนบ้างที่ฉันสามารถเห็นการดำเนินการนี้
Nic

@Nic: การส่งกระแสข้อมูลไปยังวัตถุบัฟเฟอร์ไม่ใช่รหัสที่ซับซ้อนโดยเฉพาะ แต่จริงๆถ้าคุณกำลังเพียงการพูดคุยประมาณ 50 spites ว่าเป็นอะไร ราคาต่อรองเป็นเรื่องดีที่เป็นรูปวาดภูมิประเทศของคุณที่ก่อให้เกิดปัญหาด้านประสิทธิภาพดังนั้นการเปลี่ยนไปใช้บัฟเฟอร์แบบคงที่สำหรับสิ่งนั้นอาจจะดีพอ
Nicol Bolas

อันที่จริงถ้าการอินสแตนซ์ทำงานอย่างที่เราคิดว่ามันน่าจะเป็นทางออกที่ดีที่สุด - แต่เนื่องจากมันไม่ได้การอบอินสแตนซ์ทั้งหมดลงใน vbo แบบคงที่เดียวเป็นวิธีที่จะไป
Jari Komppa
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.