เครื่องมือและเทคนิคที่นิยมสำหรับการพัฒนาเกมเชิงกระบวนวิธี
(cprimozic.net)- เมื่อโปรเจกต์ฉากและเลเวล 3D ที่รันในเบราว์เซอร์ใหญ่ขึ้น ก็มีการสะสมเครื่องมือที่นำกลับมาใช้ซ้ำได้สำหรับใช้ เทคนิคเชิงกระบวนวิธีและเชิงกำเนิด กับองค์ประกอบเฉพาะของเลเวล มากกว่าการสร้างโลกทั้งใบ
- งานเท็กซ์เจอร์มีแกนหลักอยู่ที่ Triplanar Mapping และ Hex Tiling ซึ่งใช้สำหรับใส่เท็กซ์เจอร์โดยไม่ต้องมี UV หรือซ่อนแพตเทิร์นที่เกิดจากการทำซ้ำ
- ทั้งสองเทคนิคใช้
pow()กับค่าน้ำหนักการอินเทอร์โพเลตเพื่อเพิ่มสัดส่วนของแกนหลักหรือผลลัพธ์จากการ lookup ที่เด่นกว่า และลดภาระด้านประสิทธิภาพด้วยการ ข้ามการ lookup เท็กซ์เจอร์ บางส่วน - fragment shader ที่มีต้นทุนสูงสามารถบรรเทาได้ด้วย Depth Pre-Pass และในฉากที่มี overdraw มาก ประสิทธิภาพอาจดีขึ้นได้มากกว่า 30%
- ในฝั่ง mesh และ geometry มีแนวโน้มที่จะขยายการแสดงผลของการตกแต่ง ฉากหลัง และความเสียหาย ผ่าน LoD terrain, runtime mesh processing pipeline และในอนาคตคือ Constructive Solid Geometry
เครื่องมือเชิงกระบวนวิธีที่สะสมมาจากฉาก 3D บนเบราว์เซอร์
- ตลอดหลายปีของการสร้าง 3D scenes and levels ที่รันในเบราว์เซอร์ เดโมอิสระที่เน้น custom shader ได้เติบโตเป็นรูปแบบคล้ายเกมที่เชื่อมโยงกัน
- แนวทางร่วมคือการใช้ เทคนิคเชิงกระบวนวิธีและเชิงกำเนิด กับบางส่วนของเลเวล แทนที่จะสร้างโลกทั้งใบแบบ procedural generation
- เครื่องมือและเอฟเฟกต์แบบ procedural และกึ่ง procedural ที่ใช้ซ้ำในหลายเลเวลจึงค่อย ๆ สะสมขึ้นเอง
Shader และเท็กซ์เจอร์
- เท็กซ์เจอร์ส่วนใหญ่เป็น seamless texture ที่ tile ได้ต่อเนื่องทั้งสองแกน และเมื่อปูบนพื้นที่กว้าง แพตเทิร์นที่ซ้ำกันอาจเห็นได้ชัด
- เพิ่มหลายฟีเจอร์ให้ custom shader ที่ต่อยอดจาก
MeshPhysicalMaterialของ Three.JS เพื่อปรับปรุงการรองรับ seamless texturing -
Triplanar Mapping
- Triplanar Mapping เป็นเครื่องมือหลักด้าน texturing ที่ใช้ในแทบทุกเลเวล
- สามารถใช้ seamless texture ใส่เท็กซ์เจอร์ให้ mesh ได้โดยไม่ต้องมี UV map ที่กำหนดไว้ล่วงหน้า จึงมีประโยชน์ในกรณีอย่าง terrain ที่สร้างแบบ procedural ซึ่งโมเดลเลอร์ไม่มีโอกาสกำหนด UV mapping
- ทำงานได้ดีกับทั้ง mesh ที่สร้างขึ้นและ mesh ที่โมเดลด้วยมือ
- การ implement เบาและเรียบง่าย โดยมีตัวอย่างอ้างอิงอยู่ที่ triplanarMapping.ts
-
การปรับปรุง Triplanar Mapping
- Triplanar Mapping แบบทั่วไปจะ linearly blend การ lookup เท็กซ์เจอร์จากสามแกนโดยอิงจาก normal ของ fragment
- ในบริเวณที่ normal ไม่ได้ใกล้กับแกนใดแกนหนึ่งอย่างชัดเจน เท็กซ์เจอร์อาจดูเป็นชั้น ๆ
- หากใช้
pow()ที่มีเลขชี้กำลังสูงกับค่าน้ำหนัก แล้ว normalize อีกครั้ง จะทำให้สัดส่วนของ แกนหลัก มากขึ้นและพื้นที่เปลี่ยนผ่านเล็กลง - การแปลงนี้ทำให้บน mesh ส่วนใหญ่ ค่าน้ำหนักของแกนหนึ่งเข้าใกล้ 1 และค่าน้ำหนักของอีกสองแกนเข้าใกล้ 0
- ด้วยการ ข้ามการ lookup เท็กซ์เจอร์ ของค่าน้ำหนักที่เล็กกว่า threshold สามารถลดภาระด้านประสิทธิภาพของ Triplanar Mapping ลงมาเหลือมากกว่าการทำ texturing แบบใช้ UV ปกติเพียงเล็กน้อย
- การจัดการ normal map ต้องพิจารณาแยกใน shader code และใช้ แนวทางของ GPU Gems
- รายละเอียดการ implement ดูได้ที่ Normal Mapping for a Triplanar Shader
-
Hex Tiling
- Hex Tiling เป็นอัลกอริทึมสำหรับซ่อนการ tile และการซ้ำที่เห็นได้ชัดของ seamless texture
- เพียงเพิ่ม option การตั้งค่าเดียวให้ material ก็อาจทำให้ฉากเปลี่ยนจากสภาพที่ดูเหมือน mockup คุณภาพต่ำไปใกล้เคียง semi-realistic ได้
- implementation แรกอิงจาก Shadertoy ของ Fabrice Neyret และแปลงให้เข้ากับ material system ของ Three.JS ก่อนรวมเข้ากับ shader material หลักของโปรเจกต์
- ต่อมาหลังได้รับอนุญาต จึง port เป็นไลบรารีอิสระ three-hex-tiling ที่สามารถเพิ่ม Hex Tiling ให้ built-in material ของโปรเจกต์ Three.JS ได้
- ต่างจาก Triplanar Mapping ตรงที่ต้องมี UV mapping ที่กำหนดไว้ล่วงหน้า
- หากใช้สองเทคนิคร่วมกัน จำนวน texture fetch สูงสุดต่อ fragment ต่อ map จะเพิ่มได้ถึง 27 ครั้ง จึงไม่เหมาะในทางปฏิบัติ
- Hex Tiling ก็ linearly interpolate ผลจากการ lookup สามครั้งต่อ fragment เช่นกัน ดังนั้นเทคนิคค่าน้ำหนัก
pow()ที่ใช้กับ Triplanar Mapping จึงช่วยปรับปรุงทั้งประสิทธิภาพและคุณภาพผลลัพธ์ได้
-
Depth Pre-Pass
- เทคนิค texturing ขั้นสูงอาจทำให้เกิด fragment shader ที่มีต้นทุนสูงในฉากขนาดใหญ่
- Depth Pre-Pass คือวิธีเรนเดอร์ทั้งฉากก่อนด้วย material ที่เรียบง่ายและราคาถูกมาก เพื่อบันทึก depth ของแต่ละพิกเซล
- แม้มี overhead จากการเรนเดอร์ฉากสองรอบ แต่ในฉากที่มี overdraw มาก โดยทั่วไปผลได้มักมากกว่าต้นทุน
- เมื่อมี overdraw มาก การเพิ่ม Depth Pre-Pass อาจทำให้ประสิทธิภาพดีขึ้นได้มากกว่า 30%
- หากเปลี่ยนการตั้งค่า pre-pass จะสามารถเรนเดอร์เฉพาะ fragment ที่ถูกบดบัง เพื่อ visualise fragment ที่จะถูกข้ามเมื่อใช้ pre-pass
- รายละเอียดการ implement และการตั้งค่าใน Three.JS ดูได้จาก dedicated article
-
การสังเคราะห์เท็กซ์เจอร์ PBR ด้วย AI
- AI-generated texture ถูกใช้ในแทบทุกฉาก
- หากใช้แบบพอดี ผลลัพธ์อาจดูค่อนข้างดี และเท็กซ์เจอร์ทั้งหมดในฉากตัวอย่างเป็น AI-generated
- กระบวนการสร้างเท็กซ์เจอร์ สร้าง PBR map และรวมเป็น seamless 4K texture โดยไม่ต้อง upscale กล่าวถึงไว้ใน บทความแยก
- เว็บไซต์ที่บทความนั้นกล่าวถึงสำหรับสร้าง PBR map ไม่สามารถใช้งานได้อีกแล้ว
- ปัจจุบันใช้ DeepBump สำหรับสร้าง normal map และเมื่อจำเป็นก็ใช้เครื่องมือที่ไม่ใช่ AI อย่าง Materialize กับ map อื่น ๆ
-
Volumetric Fog/Clouds
- volumetric rendering เป็นสาขาที่ได้รับความสนใจ เพราะสามารถเพิ่มเอฟเฟกต์เฉพาะตัวให้ฉากได้
- สร้าง shader ที่ค่อนข้างใช้ได้ทั่วไป ซึ่งสามารถเพิ่ม cloud หรือ fog ให้กับฉาก Three.JS ใด ๆ ได้
- ได้แรงบันดาลใจจาก Shadertoy ของ Inigo Quilez เพื่อสร้าง volumetric clouds shader พื้นฐานที่ใช้ LoD noise lookup คล้ายกัน แล้วภายหลังขยายให้เป็นรูปแบบที่ทั่วไปกว่าและตั้งค่าได้มากขึ้น
- shader นี้มีประโยชน์ในการเติมพื้นที่ว่างของเลเวลที่ค่อนข้างโล่ง และเพิ่ม ความรู้สึกมีพลวัต ให้เลเวลที่นิ่ง ด้วย cloud หรือ fog ที่เคลื่อนไหว
- ยังใช้โค้ดและแนวทางบางส่วนที่ n8programs พัฒนาในโปรเจกต์
three-good-godrays three-good-godraysก็ถูกใช้บ่อยเช่นกัน และเพิ่มบรรยากาศที่โดดเด่นมากให้เลเวล
Mesh และ Geometry
- การสร้าง mesh ขณะ runtime เป็นพื้นที่ที่ถูกทำงานมากขึ้นเรื่อย ๆ
- ชอบแนวคิดที่โลกงอกขึ้นจาก software seed แต่ต้องการหลีกเลี่ยงปรากฏการณ์ “ไม่มีที่สิ้นสุดแต่กลวงเปล่า” ของเกมบางเกมที่ชู procedural generation
- ดังนั้นจึงมุ่งเน้นเป็นหลักที่การเพิ่ม การตกแต่ง ฉากหลัง และ procedural flourish ให้เลเวล มากกว่าการสร้างประสบการณ์หลักทั้งหมดแบบ procedural
-
LoD Terrain
- terrain generation เป็นพื้นที่ตัวอย่างของการพัฒนาเกมเชิงกระบวนวิธี และตัว implementation เองก็ไม่ได้พิเศษ
- เช่นเดียวกับวิธีส่วนใหญ่ ใช้ noise function สร้าง terrain heightmap แล้ว tessellate เป็น triangle เพื่อเรนเดอร์
- การ texturing ใช้ Triplanar Mapping หรือ Hex Tiling
- หัวใจสำคัญคือ ระบบ LoD ซึ่งสร้าง terrain เป็นหน่วย tile และสร้างแต่ละ tile ในหลายความละเอียด
- จะสลับความละเอียดต่าง ๆ แบบไดนามิกตามระยะห่างระหว่าง tile กับ camera
- terrain generation system นี้ถูกนำกลับมาใช้ซ้ำบ่อย และด้วยระบบที่ยืดหยุ่นและมีประสิทธิภาพ จึงนำไปใช้กับหลายเลเวลได้โดยใช้ความพยายามน้อย
-
Pipeline การประมวลผลและปรับแต่ง mesh แบบ procedural
- ส่วนที่ทำงานมากที่สุดช่วงหลังคือ pipeline สำหรับ procedural mesh processing
- เป้าหมายเริ่มต้นคือการ subdivide และ deform low-poly mesh แบบ procedural รวมถึง mesh ที่สร้างแบบไดนามิก
- จุดประสงค์คือทำให้ mesh เรียบง่ายอย่าง platform, boulder หรือโครงสร้างขนาดใหญ่ ดูสมจริงหรือน่าสนใจขึ้นเมื่อใส่ในเลเวล
- งานนี้นำไปสู่ software pipeline ที่รับ raw geometry data ใน runtime ของเบราว์เซอร์ แก้ไขได้ตามต้องการ แล้วส่งออกกลับเป็นรูปแบบที่เรนเดอร์ได้
- กระบวนการนี้ต้องพิจารณารายละเอียดเป็นพิเศษ โดยเฉพาะในการจัดการ normal
- รายละเอียดการ implement อยู่ในบทความ subdividing meshes for displacement
ตัวเลือกทดลองถัดไป: Constructive Solid Geometry
- เครื่องมือส่วนใหญ่ที่ระบุไว้ที่นี่เดิมเริ่มจาก implementation ครั้งเดียวเพื่อ use case เฉพาะ แต่ถูกนำกลับมาใช้ซ้ำในเลเวลและบริบทอื่น ๆ
- ไอเดียหลักถัดไปที่อยากลองคือ Constructive Solid Geometry
- Constructive Solid Geometry เป็นระบบที่ใช้ boolean operator กับพื้นที่ 3D
- สามารถรวม mesh สองชิ้นใด ๆ เข้าด้วยกันได้
- สามารถตัด chunk ออกจาก mesh ได้
- และทำการปรับแต่งลักษณะคล้ายกันอื่น ๆ ได้
- csg.js implement CSG toolkit ที่มี mesh primitive, boolean operator และ API ที่สะอาดไว้ในไฟล์ JavaScript ไฟล์เดียวพร้อมคอมเมนต์ ความยาวประมาณ 500 LoC
- วางแผนว่าสักวันจะ port ไลบรารีนี้ไปเป็น Rust เพื่อทำความเข้าใจวิธีทำงานให้ดีขึ้น
- หากใช้ CSG ร่วมกับ mesh processing pipeline ที่มีอยู่ ก็น่าจะสร้างผลลัพธ์ที่น่าสนใจได้มาก
- โดยเฉพาะอยากลองฟีเจอร์ที่ทำให้ mesh เกิด damage แบบ procedural
- ตัด chunk ออกจาก building หรือ bridge เพื่อจำลอง decay หรือ weathering
- สร้าง crack บน wall หรือ road
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
เมื่อก่อนเคยลองเล่นกับ procedural generation อยู่บ้าง โดยเฉพาะพยายามทำต้นไม้ที่ดูสวย แต่ชิ้นส่วนที่ผมขาดไปคือวิธีเชื่อม geometry เข้าด้วยกันให้ง่าย
การสร้างทรงกระบอกสองอันนั้นง่าย แต่การต่อสองอันเข้าด้วยกันให้ดูเป็นธรรมชาตินั้นยากมาก
ในทางทฤษฎี CSG น่าจะเติมเต็มส่วนนี้ได้ แต่การคิดปัญหาในกรอบนั้นไม่ง่าย เพราะมองเป็นแค่ลูปเพิ่ม vertex ไปเรื่อย ๆ ไม่ได้ ต้องโมเดลทุกอย่างเป็นรูปทรง 3D
ผมเคยลองทำ routine ที่รับ vertex loop สองชุดแล้วเพิ่ม face เพื่อเชื่อมด้วย heuristic แต่การเลือกว่าควรเชื่อม vertex ไหนเข้าด้วยกันยากกว่าที่คิดมาก และได้รอยเชื่อมที่ดูแย่ได้ง่าย
สักวันหนึ่งอยากทำเกมที่ระบบ procedural generation แบบโมดูลาร์หลายระบบทำงานร่วมกันได้โดยยังคงความฉับพลันไว้ เช่น ให้ระบบต่าง ๆ “ยึดครอง” ส่วนหนึ่งของโลก แล้วมอบหมายส่วนนั้นต่อให้ระบบอื่น หรือเชื่อมเข้ากับองค์ประกอบรอบ ๆ อย่างเป็นธรรมชาติ
เมื่อเร็ว ๆ นี้มีวิดีโอ [0] เกี่ยวกับการสร้างต้นไม้แบบ generative ซึ่งวิธีแก้คือปล่อยให้ทรงกระบอกตัดกันไปเลย วิธี hacky แบบนี้ก็ใช้ได้ดีและให้ผลลัพธ์ที่โอเคได้
อย่างที่บอก CSG ก็ทำได้ แต่อาจซับซ้อนเกินจำเป็น อีกวิธีคือสร้าง skeleton ของต้นไม้ก่อนแล้วทำ lofting และถ้าจำเป็นก็ค่อยผสานกับ CSG เพื่อสร้างลำต้นกับเปลือก
ไลบรารีดี ๆ จะเปิดความเป็นไปได้ใหม่ ๆ ถ้าหาไลบรารี geometry 3D ที่ทำ boolean operation อย่างการรวมทรงกระบอกหรือการลบ geometry ได้ ก็จะลองไอเดียใหม่ ๆ ได้มากมาย ผมเคยลองมาหลายตัว แต่ตัวเดียวที่ค่อนข้างถูกใจคือ JSCAD [1]
[0] https://youtu.be/8zMbJmuwEUc?si=KQclrVPeSrIRmsbA
[1] https://github.com/jscad/OpenJSCAD.org
คล้ายกับวิธีบรรยายและแปลงรูปทรง 3D แบบ functional
ถ้าอยากดูว่าคนใน demoscene ทำอะไรกับสิ่งนี้ไว้บ้าง ขอแนะนำอย่างยิ่งให้ค้นหา Mercury Delight บน YouTube ใน Shadertoy ก็มีตัวอย่างเยอะ และมีหลายอย่างที่น่าทึ่งจริง ๆ
https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Condition...
ตัวอย่างด้านล่างก็น่าดูเช่นกัน
https://github.com/MaxBondABE/batteries/blob/master/src/geom...
การสร้างภูมิประเทศ ที่ดี ไม่ว่าจะในสเกลระดับโลกหรือระดับสายตาคน ไม่ใช่เรื่องเล็กน้อยเลย
แนวทางง่าย ๆ ก็อย่างที่เห็นในบทความ คือสร้าง heightmap ที่ขรุขระ ซึ่งแทบไม่เหมือนภูมิประเทศจริง และก็ไม่น่าสนใจนักสำหรับการสำรวจ
ตัวอย่างเช่น Dwarf Fortress เริ่มจาก midpoint displacement พื้นฐาน แต่หลังจากนั้นผ่าน post-processing แบบปรับแต่งเฉพาะจำนวนมาก
สิ่งที่อยู่ในบทความนี้ ไม่มีอะไรที่เรียกว่าเล็กน้อยได้เลยแม้จะพูดแบบกว้าง ๆ ผู้เขียน ในแง่ของการเรนเดอร์ด้วย GPU shader เพียงอย่างเดียว ถือว่าใกล้เคียงกับ นักพัฒนา 100x อย่างชัดเจนเมื่อเทียบกับแทบทุกคนบนโลก
“แนวทางง่าย ๆ” นี่ง่ายจริงหรือ?
จากคน 8 พันล้านคน จะมีสักกี่เปอร์เซ็นต์ที่ implement ได้แม้แต่ขั้น “Hello World” ที่จำเป็นต่อการเริ่มบทความนี้? แล้วจะมีสักกี่คนที่รู้ว่า shader คืออะไร? งาน OpenGL ก็ไม่ได้มีมากมายขนาดนั้น แถมสถานการณ์แบบ “OpenGL คืออะไร เราใช้แต่ Unity” ก็พบได้ทั่วไป
แล้วเกมเมอร์ออนไลน์ล่ะ? ณ วันที่ 28 กรกฎาคม 2024 เวลา 13:22 น. EST จากผู้เล่น 1,021,282 คน [1] ที่กำลังออนไลน์ใน Counter Strike 2 จะมีสักกี่เปอร์เซ็นต์ที่ implement ได้แม้แต่ขั้นแรกของ shader ที่เกมที่พวกเขาเล่นต้องใช้?
จะมีสักกี่เปอร์เซ็นต์ที่คอมไพล์โปรแกรม C++ บน command line แบบง่าย ๆ หรือเขียนสคริปต์ JavaScript ที่ง่ายกว่านั้นในเบราว์เซอร์ได้? จริง ๆ นี่เป็นคำถามดักนิดหน่อย เพราะคนส่วนใหญ่แค่ใช้อีเมลก็แทบไม่คล่องแล้ว
[1] https://steamdb.info/app/730/charts/
เช่นเดียวกับ AI การสร้าง procedural content ที่ดีที่สุดก็น่าจะมี post-processing แบบปรับแต่งเฉพาะอยู่ในผลลัพธ์สุดท้าย
เป็นบทความที่สรุป เทคนิค procedural หลายอย่างที่ใช้บนเว็บได้ดีมาก จึงมีประโยชน์จริง ๆ
ถ้าสนใจ procedural RPG level editor ที่กำลังทำอยู่ ดูได้ที่ https://github.com/gamedevgrunt/3D-Action-RPG-JavaScript
ประสบการณ์ของผมกับการใช้ depth pre-pass ค่อนข้างปนกัน
จากที่ลองหลายครั้ง บน GPU เดสก์ท็อประดับกลางถึงสูง ผมไม่เห็นการเพิ่มประสิทธิภาพที่ชัดเจน
ไม่แน่ใจเหตุผลที่แน่ชัด แต่คิดว่าอาจเป็นเพราะ early Z rejection ช่วยลดการเรียก pixel shader อยู่แล้ว โดยปกติจะเรนเดอร์ mesh ทึบแสงจากหน้าไปหลัง
อย่างไรก็ดี การทดลองของผมอยู่ในบริบทของแอปพลิเคชัน CAD/CAM ไม่ใช่เกม ฉากก็ค่อนข้างต่างจากสภาพแวดล้อมเกมทั่วไปด้วย คือมี texture น้อย แต่ geometry มีจำนวน polygon สูงมาก
depth pre-pass มักเป็นขั้นแรกของ occlusion culling ด้วย แต่เรื่องนี้ก็ขึ้นกับบริบทเช่นกัน มีแนวโน้มว่าจะมีประโยชน์กับทิวทัศน์เมืองที่ซับซ้อนมากกว่าโมเดล CAD
มันมีประโยชน์ที่สุดเมื่อ depth complexity สูงและ fragment shader แพง พูดอย่างเป็นธรรม เกมส่วนใหญ่ก็เข้าข่ายนี้
ใน deferred renderer โดยทั่วไปไม่จำเป็น แต่ใน forward+ มักได้ประโยชน์ค่อนข้างมาก