การเรนเดอร์คลื่นทะเลแบบ FFT ที่พัฒนาใน Godot
(github.com/2Retr0)- GodotOceanWaves เป็นการทดลองเรนเดอร์ใน Godot Engine ที่สร้างผิวน้ำทะเลเปิดด้วยการใช้ inverse Fourier transform ของ สเปกตรัมคลื่นทะเลแบบมีทิศทาง และจำลองสภาพแวดล้อมทางทะเลหลากหลายแบบด้วยพารามิเตอร์ที่ปรับผ่านสคริปต์ได้แบบเรียลไทม์
- Gerstner waves เหมาะกับรายละเอียดความถี่ต่ำของน้ำที่สงบ แต่มีข้อจำกัดสำหรับผิวน้ำทะเลเปิดที่รุนแรง โปรเจกต์นี้จึงใช้สเปกตรัมที่อิงข้อมูลสมุทรศาสตร์เชิงประจักษ์และ FFT
- การสร้างคลื่นทำตามแนวทางของ Tessendorf โดยสร้างสถานะสเปกตรัมเริ่มต้น จากนั้นแพร่กระจายตามเวลาด้วยความสัมพันธ์การกระจายตัว แล้วใช้ inverse Fourier transform เพื่อสร้าง displacement, normal และ foam map
- การเรนเดอร์อิงตาม BSDF จากการนำเสนอ Atlas ที่ GDC แต่ใช้การแจกแจง GGX ส่วน foam จะสะสมและสลายตัวในบริเวณที่ Jacobian ของ displacement เป็นค่าลบ และ sea spray พัฒนาด้วย GPUParticles3D ของ Godot พร้อม shader แบบกำหนดเอง
- รองรับ wave cascade หลายชุด การปรับรอบการอัปเดต และการกระจายโหลดแยกตาม cascade เพื่อจัดการ tiling และภาระงานของ GPU แต่ cascade ต้องใช้ความละเอียดของสเปกตรัมเท็กซ์เจอร์เดียวกัน และวิธีสร้างอนุภาค sea spray มีประสิทธิภาพต่ำเมื่อเพิ่มความหนาแน่น
GodotOceanWaves ทำอะไร
- GodotOceanWaves เป็นโปรเจกต์ทดลองเรนเดอร์ทะเลเปิดใน Godot Engine
- การสร้างคลื่นใช้ inverse Fourier transform ของสเปกตรัมคลื่นทะเลแบบมีทิศทาง
- พารามิเตอร์ที่เปิดให้ปรับถูกจัดไว้อย่างกระชับ และสามารถแก้ไขแบบเรียลไทม์ผ่านสคริปต์เพื่อจำลองสภาพคลื่นทะเลหลายรูปแบบได้
- มีวิดีโอเดโม
ocean_demo.mp4และenvironment_demo.mp4รวมอยู่ด้วย
เหตุผลที่ใช้คลื่นแบบ Fourier แทน Gerstner waves
- วิธีทั่วไปในการทำอนิเมชันน้ำในเกมคือการเลื่อนตำแหน่งเวอร์เท็กซ์ด้วย Gerstner waves
- Gerstner waves เหมาะกับการจำลองรายละเอียดความถี่ต่ำของน้ำที่สงบ แต่ยังไม่เพียงพอในการแสดงผิวน้ำทะเลเปิดที่ขรุขระอย่างแม่นยำ
- เมื่อใช้สเปกตรัมคลื่นทะเลที่อิงข้อมูลเชิงประจักษ์ซึ่งนักสมุทรศาสตร์เก็บรวบรวมไว้ จะสามารถจัดการได้ตรงขึ้นว่าต้องเปลี่ยนคลื่นและพารามิเตอร์ใดเพื่อสร้างสภาพทะเลแบบหนึ่ง ๆ
- FFT มีความซับซ้อน
O(N log N)ซึ่งต่ำกว่า discrete Fourier transform แบบคลาสสิกที่เป็นO(N^2)และสามารถขยายไปยังระบบขนานได้ จึงเหมาะกับการรันบน GPU - ใน Gerstner waves แต่ละเธรดต้องคำนวณกับจำนวนคลื่น
Nลูก แต่ในคลื่นแบบ FFT แต่ละเธรดทำการคำนวณเพียงระดับlog(N)เท่านั้น
Shading, foam และ sea spray
- โมเดลแสงของทะเลโดยรวมอิงตาม BSDF จาก การนำเสนอ Atlas ที่ GDC
- การแจกแจงไมโครแฟซิตใช้ GGX แทน Beckmann
- เพราะไฮไลต์ของ GGX ที่แบนและนุ่มกว่าให้ภาพรวมที่สม่ำเสมอกว่าในสภาพคลื่นทะเลหลายแบบที่ทดสอบ
- normal/foam map ถูกสุ่มตัวอย่างโดยผสมการกรองแบบ bicubic และ bilinear ตามความหนาแน่นพิกเซลใน world space
- ช่วยลดอาร์ติแฟกต์ texture aliasing เมื่อความละเอียดพื้นผิวต่ำ และรักษารายละเอียดเมื่อความละเอียดพื้นผิวสูง
- Sea foam ตัดสินใจว่าจะสร้างหรือไม่ตามวิธีของ Tessendorf ในตำแหน่งที่ยอดคลื่นม้วนเข้าด้านใน กล่าวคือบริเวณที่ Jacobian ของ displacement เป็นค่าลบ
- foam จะสะสมแบบเชิงเส้นลงในเท็กซ์เจอร์ตลอดการอัปเดตคลื่นหลายครั้ง
- พารามิเตอร์
"foam grow rate"และ"foam decay rate"ควบคุมการสะสมและการสลายตัวแบบเอ็กซ์โปเนนเชียลตามลำดับ
- Sea spray พัฒนาด้วยโหนด
GPUParticles3Dของ Godot และ particle shader แบบกำหนดเอง- อนุภาคกระจายอย่างสม่ำเสมอบนระนาบภายใน bounding box ของ
GPUParticles3D - อนุภาคจะถูก culling ตามปริมาณ foam ณ ตำแหน่งนั้น และอนุภาคที่ไม่ถูก culling จะเริ่มวงจรชีวิตจากออฟเซ็ตแบบสุ่ม
- อนุภาคแต่ละตัวใช้ billboard sprite ที่ใช้เท็กซ์เจอร์คงที่หนึ่งภาพ
- ระหว่างวงจรชีวิต จะเปลี่ยน scale และ displacement เพื่อจำลองรูปลักษณ์ของ splash และใช้เอฟเฟกต์ dissolve ใน mesh shader เพื่อให้เกิดความรู้สึกว่าน้ำกระเซ็นแตกเป็นละอองกลางอากาศ
- อนุภาคกระจายอย่างสม่ำเสมอบนระนาบภายใน bounding box ของ
- ข้อเสียใหญ่ของวิธี sea spray นี้คือ ต่อให้เพิ่มจำนวนอนุภาคมาก ความหนาแน่นก็เพิ่มขึ้นเพียงเล็กน้อย
- เพราะอนุภาคกระจายสม่ำเสมอใน bounding box ทำให้อนุภาคที่เพิ่มมาส่วนใหญ่ถูก culling
Pipeline การจำลองคลื่น
- วิธีสร้างคลื่นผิวน้ำทำตาม Tessendorf อย่างใกล้ชิด
- สร้างสถานะทะเลในสเปกตรัมเริ่มต้นโดยนำฟังก์ชันสเปกตรัมคลื่นทะเลแบบมีทิศทางมาคูณกับตัวเลขสุ่มที่มีการแจกแจง Gaussian
- สถานะเริ่มต้นจะถูกแพร่กระจายตามเวลาโดยใช้ ความสัมพันธ์การกระจายตัว ที่เชื่อมความถี่ของคลื่นกับความเร็วการแพร่กระจาย
- นำ inverse Fourier transform ไปใช้กับสถานะที่แพร่กระจายแล้วเพื่อสร้าง displacement map และ normal map
- การพัฒนาประกอบเป็น compute shader pipeline ที่ใช้ abstraction
RenderingDeviceของ Godot
องค์ประกอบของสเปกตรัมคลื่นทะเล
- ฟังก์ชันสเปกตรัมคลื่นทะเลแบบมีทิศทาง
S(ω, θ)คืนค่าพลังงานของคลื่นเมื่อกำหนดความถี่ωและทิศทางθ - ฟังก์ชันนี้ประกอบด้วยฟังก์ชันสเปกตรัมแบบไม่มีทิศทาง
S(ω)และฟังก์ชันการกระจายทิศทางD(ω, θ)โดยตัวเลือกทั้งสองเป็นอิสระต่อกัน - สำหรับสเปกตรัมแบบไม่มีทิศทาง ใช้สเปกตรัม Texel-Marsen-Arsloe(TMA) ที่ Horvath อธิบายไว้
- TMA ใช้ความเร็วลม
U, ความลึกน้ำDและ fetch lengthF - TMA ผสานสเปกตรัม JONSWAP ก่อนหน้าเข้ากับฟังก์ชันลดทอนตามความลึกน้ำ
- TMA ใช้ความเร็วลม
- สำหรับฟังก์ชันการกระจายทิศทาง ใช้วิธีผสมการกระจายทิศทางแบบ flat และ Hasselmann ของ Horvath ด้วยพารามิเตอร์
spreadคือμ- รวมพารามิเตอร์
swellคือξที่ Horvath เสนอไว้ด้วย เพื่อจำลอง elongation ของคลื่นทะเล - เลือกการกระจายทิศทางแบบ Hasselmann เพราะให้คำตอบเชิงวิเคราะห์แบบประมาณสำหรับค่าสัมประสิทธิ์ normalization
Q(σ)
- รวมพารามิเตอร์
- ตามข้อเสนอของ Tessendorf ฟังก์ชันสเปกตรัมผลลัพธ์ยังคูณด้วยเทอมกดทอนคลื่นขนาดเล็กด้วย
- เทอมนี้ใช้ขนาดของเวกเตอร์ wave number
kและพารามิเตอร์detailคือδ
- เทอมนี้ใช้ขนาดของเวกเตอร์ wave number
- สเปกตรัมคลื่นทะเลแบบมีทิศทางขั้นสุดท้ายผสาน TMA, mixed directional spread และ small-wave suppression เข้าด้วยกัน
การพัฒนา GPU FFT
- FFT ใช้ การพัฒนา GPU แบบกำหนดเอง ที่เขียนด้วย compute shader
- ใช้อัลกอริทึม Stockham FFT แทน Cooley-Tukey เพื่อหลีกเลี่ยง bit-reversal permutation ตอนเริ่มต้น
- ตาม Flügge จะคำนวณเท็กซ์เจอร์
"butterfly"ที่เข้ารหัสการไหลของข้อมูลของ FFT- เท็กซ์เจอร์นี้จะถูกคำนวณหนึ่งครั้งทุกครั้งที่ความละเอียดของสเปกตรัมเท็กซ์เจอร์เปลี่ยน
- 2D FFT เริ่มจากใช้เคอร์เนล FFT ในทิศทางแถวของสเปกตรัมเท็กซ์เจอร์
- จากนั้น transpose เท็กซ์เจอร์ด้วย compute shader เพื่อใช้เคอร์เนล FFT ทิศทางแถวเดิมซ้ำในลักษณะ FFT ทิศทางคอลัมน์
- การ transpose ไม่เพียงช่วยให้ใช้ pipeline ซ้ำได้ แต่ยังปรับปรุงรูปแบบการเข้าถึงหน่วยความจำด้วย
Wave cascade และการรับมือ tiling
- ในระยะไกล โดยเฉพาะเมื่อมี sea foam อาร์ติแฟกต์แบบ tiling จะเห็นได้ชัดมาก
- ระบบสร้างคลื่นจัดการปัญหานี้ด้วยการเลเยอร์ wave cascade หลายชุดพร้อมกัน
- แต่ละ cascade มีขนาด tiling และชุดพารามิเตอร์ของตัวเอง
- สามารถเพิ่มหรือลบ cascade แบบไดนามิกได้แบบเรียลไทม์
- เนื่องจาก cascade ทั้งหมดใช้ compute pipeline เดียวกัน จึงต้องมี ความละเอียดของสเปกตรัมเท็กซ์เจอร์ เท่ากัน
- อีกทางเลือกหนึ่งคือ blend wave displacement และ normal กับ noise เพื่อลดอาร์ติแฟกต์แบบ tiling ด้วยต้นทุนประสิทธิภาพที่ต่ำกว่า
- เมื่อนำ cascade มาซ้อนกัน ต้องเลือกพารามิเตอร์และขนาดของแต่ละ cascade อย่างระมัดระวังเพื่อหลีกเลี่ยงการแทรกสอดกันของคลื่น
- phase ของคลื่นในแต่ละ cascade ก็ควรถูก offset เพื่อไม่ให้แทรกสอดกัน
- ระบบพยายามทำสิ่งนี้โดยอัตโนมัติด้วยการ offset เวลาเริ่มต้นของแต่ละ cascade ให้ต่างกัน
- มี caveat ระบุว่าไม่แน่ชัดว่าวิธีนี้ใช้งานได้จริงหรือไม่
การกระจายโหลดการอัปเดต
- เนื่องจากการเคลื่อนที่ของคลื่นไม่สม่ำเสมอ แม้ไม่อัปเดต displacement ทุกเฟรม ก็ยังอาจดูราบรื่นในเชิงการรับรู้ได้
- พารามิเตอร์
"update rate"ควบคุมว่าจะอัปเดต wave cascade บ่อยแค่ไหนต่อวินาที - การข้ามเฟรมช่วยลดเวลา GPU ที่ใช้กับ FFT แต่ในเฟรมที่ pipeline สร้างคลื่นทำงาน ก็ยังเกิด stutter ได้
- การทดลองคำนวณการอัปเดต cascade แบบอะซิงโครนัสด้วย local
RenderingDeviceของ Godot ทำให้เกิด overhead ด้านประสิทธิภาพสูงจากการส่งต่อเท็กซ์เจอร์ระหว่าง CPU และ GPU - ดังนั้นระบบจึงพยายามทำ การกระจายโหลด ของ cascade แทน
- ถ้าเวลาเฟรมสั้นกว่า update rate จะอัปเดต cascade เพียงหนึ่งชุดต่อเฟรม
- วิธีนี้ลด stutter พร้อมรักษาประโยชน์ของภาระงาน GPU ที่ต่ำลงจากการข้ามเฟรมไว้ได้
เอกสารอ้างอิง
- Realtime GPGPU FFT Ocean Water Simulation: Fynn-Jorin Flügge, Hamburg University of Technology, 2017
- I Tried Simulating The Entire Ocean: Garrett Gunnell, 2023
- Empirical Directional Wave Spectra for Computer Graphics: Christopher J. Horvath, DigiPro, 2015
- Simulating Ocean Water: Jerry Tessendorf, SIGGRAPH, 2004
- Implementing Fast Fourier Transform Algorithms of Real-Valued Sequences: Robert Matusiak, Texas Instruments, 2001
- Wakes, Explosions and Lighting: Interactive Water Simulation in 'Atlas': Mark Mihelich, GDC, 2019
- FFT-Ocean: Ivan Pensionerov, GitHub, 2020
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
รีโพซิทอรี Godot อีกสองอันที่คนนี้ทำก็น่าสนใจมากเหมือนกัน ชอบตรงที่ใส่รายละเอียดไว้เยอะเวลาอธิบายรีโพซิทอรี โดยเฉพาะอันนี้ที่สะดุดตา: https://github.com/2Retr0/GodotGaussianSplatting
สงสัยว่าเป็นนักศึกษาหรือเปล่า เขาอ้างอิงงานอื่นอยู่บ่อย ๆ และดูเหมือนเข้าใจเอกสารที่เพิ่งเผยแพร่ล่าสุดได้ค่อนข้างดี
ENGR96A-coursework,Relevant coursework for ENGR 96A Introduction to Engineering Design F23และดูจากวันที่ commit แล้ว F23 น่าจะหมายถึงเทอมฤดูใบไม้ร่วงปี 2023แน่นอนว่า คอร์สของ UCLA ที่กล่าวถึงในรีโพซิทอรีอาจเปิดให้ทุกคนเข้าถึงได้ก็ได้ ดังนั้นอาจไม่จำเป็นต้องเป็นนักศึกษาที่ลงทะเบียนกับ UCLA ในความหมายแบบดั้งเดิมถึงจะเรียนได้
เมื่อ 20 ปีก่อน ต่อให้ปรับ ผิวน้ำทะเล ในงานเรนเดอร์กันเป็นเดือน ๆ ก็คงยังไปไม่ถึงใกล้ระดับนี้เลย ผลงานทำออกมาได้ดีจริง ๆ
แต่คลิปเดโมดูเหมือนจะโอเวอร์ไปนิด จากมุมมองของคนที่มีบันทึกการเดินเรือทะเลเปิดมากกว่า 50,000 ไมล์ทะเล ถ้าคลื่นแหลมและสูงแบบนั้น ลมน่าจะต้องพัดแรงกว่านี้มาก ถึงอย่างนั้นก็น่าจะเป็นส่วนที่ปรับพารามิเตอร์ได้
ถ้าทำด้วย Godot แบบนี้ถือว่าเป็นการเรนเดอร์แบบเรียลไทม์ได้ไหม? ก็สงสัยเหมือนกันว่าต้องใช้ GPU ระดับสัตว์ประหลาดหรือเปล่า
อีกอย่าง ละอองน้ำทะเลในนี้ดูเหมือนเป็นเอฟเฟกต์หลังการประมวลผล แต่ในทะเลจริง ละอองน้ำจะทำให้ความคมของคลื่นดูทู่ลง และเผยให้เห็นได้ราง ๆ ทางสายตาด้วย
แน่นอนว่าโมเดลที่สง่างาม เรียบง่าย และได้ผลนี้แทบไม่มี ฟิสิกส์ อยู่เลย ดังนั้นขอให้มองเรื่องพวกนี้ไม่ใช่การยืนกอดอกวิจารณ์ แต่เป็นการลองจิ้ม ๆ สำรวจ และเสนอทิศทางที่น่าลองมากกว่า
หนึ่งในวิดีโอที่ชอบในหัวข้อนี้: https://m.youtube.com/watch?v=yPfagLeUa7k
อันนี้ก็ใช้ได้: https://m.youtube.com/watch?v=kGEqaX4Y4bQ
แต่กลับใช้เวลาทุกสุดสัปดาห์ไปกับการปรับ water shader เล่นจนกว่าจะได้ภาพที่ต้องการ ทั้งที่เกมของผมเป็นเกมปริศนา ไม่ต้องมีน้ำเลย ตอนนี้ก็แค่ได้หน้าจอเริ่มต้นเท่ ๆ มาเท่านั้นเอง
เดี๋ยวนี้มันเป็นแค่ช่องติ๊กหนึ่งใน game engine และยังมีฟีเจอร์แบบนั้นอีกนับไม่ถ้วน ผู้คนคงจินตนาการไม่ออกว่าเราอยู่ในสภาพแวดล้อมที่ดีแค่ไหนเพราะเอนจินสมัยใหม่
ไม่ได้ตั้งใจจะลดคุณค่าของงานนี้นะ มันเป็นความสำเร็จที่ยอดเยี่ยมจริง ๆ และขอชื่นชมผู้สร้าง แค่อยากบอกว่าเรากำลังอยู่ในยุคที่ดีมากแค่ไหน
ถึงอย่างนั้นก็ดูดีมาก และจากที่ลองค้นคร่าว ๆ เมื่อเร็ว ๆ นี้ น่าจะเป็นระดับที่ดีที่สุดในบรรดาสิ่งที่ทำได้
ส่วนตัวอยากเห็นคลื่นทะเลเปิดที่โดยปกติดูมี จังหวะ มากกว่านี้ สงสัยว่าการจำลองลม 15 นอตที่พัดเหนือผิวน้ำระยะ 1 กม. เป็นเวลาหลายชั่วโมง จะตรงกับสิ่งที่สังเกตได้จริงไหม นั่นคือภาพที่รอยแตกของคลื่นค่อนข้างเป็นระเบียบก่อตัวขึ้นแล้วกระจายตัวไป
สิ่งแบบนี้แหละที่ดึงผมเข้าสู่โลกคอมพิวเตอร์ แต่ไม่รู้ตัวเลยว่าชีวิตกลับไหลไปสู่ความง่ายและน่าเบื่อของการเอาไลบรารี endpoint และเรื่องไร้สาระแบบองค์กรมาเชื่อมต่อกัน จนพาไปสู่ burnout
หวังว่าสักวันจะกลับไปได้อีกครั้ง
ลองทำดูเถอะ ทุกคนเอาใจช่วยอยู่
แม้แนวทางจะต่างกัน แต่ถ้าสนใจ การจำลองคลื่น/มหาสมุทร Acerola ก็ทำวิดีโอเรื่องนี้ไว้อย่างยอดเยี่ยมตามสไตล์เขา https://youtu.be/PH9q0HNBjT4 และ https://youtu.be/yPfagLeUa7k
แก้ไข: หนึ่งในนั้นถูกลิงก์ไว้ในเอกสารอ้างอิงแล้ว แต่ใช้ชื่อจริงแทนนิกเนม เลยเพิ่งสังเกตเห็นตอนนี้
น่าสนใจว่าปัญหานี้ยากแค่ไหน พยายามกันมาหลายสิบปีแล้ว แต่ก็ยังติดอยู่ใน uncanny valley
ถ้าหยุดภาพฉากนี้ไว้ คลื่นที่พุ่งขึ้นมาดูเหมือนภูเขาที่ปกคลุมด้วยหิมะ น้ำจะมีรูปทรงแหลมแบบนี้ได้ต้องมีลมแรงพอสมควร แต่ลมคงไม่พัดตรงขึ้นด้านบน จึงรู้สึกไม่สมจริง ลักษณะแหลม ๆ แบบนี้ควรมีทิศทาง
ถึงอย่างนั้น คุณสมบัติของคลื่นสเวลล์ก็จับได้ค่อนข้างดี การเคลื่อนไหวขึ้นลงมากกว่าการเคลื่อนที่แนวนอน ซึ่งเป็นสิ่งที่คาดหวังได้ในทะเลเปิด
ไม่รู้ว่าเขาจำลองการส่องผ่านของแสงอย่างไร แต่มันทำงานได้ดี แสงที่ลอดผ่านระหว่างคลื่นตอนพระอาทิตย์ขึ้นหรือตกนั้นยอดเยี่ยม และทำให้เอฟเฟกต์ดูสมจริงมาก
อาจเรียกได้ว่ามีความเป็นภาพวาดอยู่บ้าง แต่ก็ไม่ได้ดูผิด
ผมเคยอยู่ริมทะเลมาหลายปี พออยู่ริมทะเล มหาสมุทรมันชวนหลงใหลและให้ความรู้สึกดีมากจนมองได้เป็นชั่วโมง ๆ เห็นอันนี้แล้วผมคงไม่มีทางคิดแม้แต่ชั่วขณะว่าเป็นงานเรนเดอร์
ใน Shadertoy ก็มีตัวอย่างที่ดีอยู่: https://www.shadertoy.com/view/Ms2SD1
Fourier เกิดไอเดียเรื่องการแปลงความถี่ของคลื่นระหว่างวัด คลื่นน้ำขึ้นน้ำลง ดังนั้นในแง่หนึ่งก็เหมือนวนกลับมาครบรอบพอดี
น่าประทับใจมาก
ยอดเยี่ยม แต่แนวทางนี้น่าจะพังลงในระดับหนึ่งเมื่อเจอกับคลื่นใหญ่มาก ๆ เพราะ ความไม่เชิงเส้น ระลอกน้ำไม่ใช่คลื่นไซน์ แต่สำหรับคลื่นเล็กและคลื่นสูงระดับกลาง มันใกล้เคียงพอให้วิธีแบบนี้ทำงานได้ดีมาก
คลื่นใหญ่มาก ๆ ไม่เพียงแต่ห่างจากคลื่นไซน์มากขึ้นเท่านั้น แต่คลื่นยังเริ่มมีปฏิสัมพันธ์กันแบบไม่เชิงเส้น ซึ่งจับด้วยวิธีซ้อนทับเชิงเส้นที่ใช้ที่นี่ไม่ได้
ดังนั้นสำหรับสภาพทะเลส่วนใหญ่ มันยอดเยี่ยมมาก แต่ถ้าอยากทำคลื่นแบบใน ‘The Perfect Storm’ ให้สมจริง ก็ต้องใช้แนวทางอื่น
น่าประทับใจมากและดีกว่าที่เคยเห็นมา แต่ คลื่นสเวลล์ ยังรู้สึกเพี้ยนไปเล็กน้อย ถ้าจะอธิบายก็คือ คลื่นความถี่สูงไม่ได้เคลื่อนที่ไปบนคลื่นความถี่ต่ำแบบในวิดีโอนั้น