• Nanite WebGPU เป็นโปรเจ็กต์ที่นำการเรนเดอร์แบบ Nanite ของ UE5 มาทำงานในเบราว์เซอร์ด้วย WebGPU โดยมี meshlet LOD hierarchy, ซอฟต์แวร์แรสเตอร์ไรเซอร์, billboard impostor และการคัลลิงระดับ instance/meshlet
  • การพัฒนามุ่งเน้นทั้งความเรียบง่ายในการพรีโปรเซสอินพุต OBJ ภายในเบราว์เซอร์ และการ ทดลองดูความเปลี่ยนแปลงของประสิทธิภาพ ด้วยการปรับค่าผ่าน checkbox และ slider
  • เดโมมีฉาก Jinx ขนาด 640M triangles และฉากอย่าง Lucy/Dragons ที่มากสุด 1.7B triangles โดยสามเหลี่ยมสีขาวใน Jinx คือผลจากการซอฟต์แวร์แรสเตอร์ไรซ์ที่บีบอัดเป็น 32 บิตเพื่อรองรับข้อจำกัดของ WebGPU
  • เนื่องจาก WebGPU ไม่มี atomic<u64> จึงยากที่จะนำซอฟต์แวร์แรสเตอร์ไรเซอร์แบบอิง visibility buffer ที่มีประสิทธิภาพมาใช้ตรง ๆ ได้ และเวอร์ชันปัจจุบันต้องเก็บ depth u16 กับ octahedron normal 2*u8 ไว้ใน 32 บิต ทำให้ต้องยอมรับ precision artifact
  • ผู้เขียนระบุว่าความท้าทายหลักของการทำระบบแบบ Nanite จริง ๆ ไม่ใช่แค่จำนวนสามเหลี่ยม แต่คือ mesh simplification และ error metric โดยการทำเองนี้ยังไม่มีส่วนอย่าง simplification, streaming, compression, visibility buffer หรือ 2-pass occlusion culling แบบ UE5

ภาพรวมโปรเจ็กต์

  • Nanite WebGPU เป็นโปรเจ็กต์ที่ใช้ WebGPU เพื่อทำการเรนเดอร์แบบ Nanite ในเว็บเบราว์เซอร์
  • องค์ประกอบหลักที่รวมไว้คือ meshlet LOD hierarchy, ซอฟต์แวร์แรสเตอร์ไรเซอร์, billboard impostor และการคัลลิงระดับ instance และ meshlet
  • รองรับโมเดลที่มี texture และ per-vertex normal พร้อมมี slider และ checkbox สำหรับแต่ละการตั้งค่า
  • WebGPU ใช้งานได้เฉพาะใน Chrome
  • แอปทั้งหมดสามารถรันแบบออฟไลน์ได้ด้วย Deno และ unit test ของ shader ก็เขียนด้วยวิธีนี้เช่นกัน

ฉากเดโมและขนาด

  • Jinx เดโม มี instance ขนาด 120×120 รวมเป็น 640M triangles
    • โมเดล Jinx เดี่ยวถูกย่อจาก 44k triangles เหลือ 3k triangles และมี root meshlets 59 ตัว
    • สามเหลี่ยมสีขาวคือสามเหลี่ยมที่ถูกซอฟต์แวร์แรสเตอร์ไรซ์ อยู่ระหว่างพื้นที่ที่ฮาร์ดแวร์แรสเตอร์ไรซ์กับ impostor ของฉากหลังที่อยู่ไกล
  • Lucy and dragons วางวัตถุสองชนิดแบบ instance อย่างละ 70×70 รวมเป็น 1.7B triangles
    • ในฉากตัวอย่าง เกือบ 98% ของสามเหลี่ยมถูกซอฟต์แวร์แรสเตอร์ไรซ์ และผู้เขียนระบุว่าเส้นทางนี้เร็วกว่า hardware มาก
  • Lucy เป็นฉาก 110×110 instance ขนาด 1.2B triangles
    • รูปปั้น Lucy เดี่ยวถูกย่อจาก 100k triangles เหลือ root meshlet เดียวที่มี 86 triangles
  • Dragons เป็นฉาก 70×70 instance ขนาด 1.2B triangles
    • มังกรเดี่ยวถูกย่อจาก 250k triangles เหลือ root meshlet เดียวที่มี 102 triangles
  • Bunnies เป็นฉาก 500×500 instance ขนาด 1.2B triangles
    • bunny เดี่ยวถูกย่อจาก 5k triangles เหลือ root meshlet เดียวที่มี 96 triangles
    • เนื่องจาก bunny มีขนาดเล็ก จึงถูก frustum culling เป็นส่วนใหญ่

ฟีเจอร์ที่พัฒนาแล้ว

  • Meshlet LOD hierarchy

    • mesh preprocessing ทำงานในเบราว์เซอร์
    • ใช้ meshoptimizer และ METIS ผ่าน WebAssembly
    • มี file exporter ให้ด้วย เพื่อไม่ต้องรอการพรีโปรเซสทุกครั้งระหว่างการรีเฟรชหน้า
  • Software rasterizer

    • WebGPU ไม่มี atomic<u64> ที่จำเป็นต่อการทำเวอร์ชันประสิทธิภาพสูง
    • เวอร์ชันปัจจุบันแพ็ก depth u16 และ octahedron-encoded normals 2*u8 ไว้ใน 32 บิต
    • ด้วยข้อจำกัด 32 บิต จึงต้องลดความแม่นยำลงมาก และเป้าหมายคือการแสดงให้เห็นว่าแรสเตอร์ไรเซอร์ทำงานได้
    • ข้อจำกัดนี้ยังกระทบ depth pyramid ที่ใช้กับ occlusion culling ด้วย
  • Billboard impostors

    • ใช้ภาพ 12 ภาพรอบแกน UP และ blend ด้วย dithering ตามตำแหน่งกล้อง
    • ไม่รองรับมุมมองจากด้านบน/ด้านล่าง
    • มีทั้ง diffuse และ normal เพื่อทำ shading ตอนรันไทม์
    • Impostors preview เป็นเดโมที่เพิ่มขนาด impostor texture

การคัลลิงและเส้นทางการเรนเดอร์

  • การคัลลิงประกอบด้วยหลายขั้น
    • Per-instance: frustum culling และ occlusion culling
    • Per-meshlet: frustum culling และ occlusion culling
    • Per-triangle: hardware backface culling และ z-buffer
  • WebGPU ไม่มี early-z
  • occlusion culling ใช้เพียง depth pyramid ที่สร้างจาก depth buffer ของเฟรมก่อนหน้า
    • ไม่มี reprojection และไม่มีวิธีแบบ two-pass
    • ผู้เขียนมองว่าแค่การทำแบบปัจจุบันก็เพียงพอที่จะคัลลิงสามเหลี่ยมจำนวนมากและประเมินผลด้านประสิทธิภาพได้แล้ว
  • สามารถสลับระหว่าง GPU-driven rendering กับ implementation แบบ CPU ที่เรียบง่ายได้
    • เวอร์ชัน CPU ยังไม่ได้ใส่ optimization มากนัก แต่สามารถไล่ดีบักทีละขั้นด้วย debugger ได้
  • ฟังก์ชัน “Freeze culling” ช่วยให้ขยับกล้องแล้วตรวจดูเฉพาะสิ่งที่ถูกวาดในเฟรมล่าสุดได้

การใช้งานและการดีบัก

  • การเคลื่อนที่ใช้ [W, S, A, D], ขึ้น/ลงใช้ [Z, SPACEBAR], และเร่งความเร็วใช้ [Shift]
  • หากเห็นผลลัพธ์แปลก ๆ สามารถลองเปิดและปิดตัวเลือกการคัลลิงเพื่อตรวจสอบได้
    • ใน implementation ยังมีบั๊กเล็กน้อยบางจุด
  • สามเหลี่ยมสีขาวคือสามเหลี่ยมที่ถูกซอฟต์แวร์แรสเตอร์ไรซ์
    • เพราะ WebGPU ไม่มี atomic<u64> จึงต้องบีบอัด depth และ normal ให้อยู่ใน 32 บิต
    • depth แบบ 16-bit อาจทำให้เกิด artifact จำนวนมาก เช่น z-fighting หรือการรั่วของภาพ
    • หากปิดซอฟต์แวร์แรสเตอร์ไรเซอร์ จะดู raw Nanite meshlets ได้ง่ายขึ้น แต่ประสิทธิภาพอาจลดลงมาก
  • ค่า FPS อาจแกว่งจาก forced VSync ของเบราว์เซอร์ และควรใช้ปุ่ม “Profile” เพื่อตรวจสอบ timing

เป้าหมายของการออกแบบ

  • เป้าหมายแรกของโปรเจกต์คือ ความเรียบง่าย
    • เริ่มจากไฟล์ OBJ และทำทุกขั้นตอนภายในแอป
    • ไม่ต้องมีขั้นตอน preprocess แยกต่างหากอย่างการ export จาก Blender
    • มุ่งให้สามารถตั้ง breakpoint ที่ loadObjFile() แล้วไล่ดูไปจนจบเฟรมแรกได้
  • เป้าหมายที่สองคือ ความเป็นงานทดลอง
    • เพราะทำเป็นหน้าเว็บแทนที่จะใช้ Rust และ Vulkan จึงสามารถคลิกลิงก์แล้วปรับ checkbox หรือ slider เพื่อดูการเปลี่ยนแปลงของประสิทธิภาพได้
    • สามารถเห็นได้ด้วยตัวเองว่า option ไหนสำคัญ เช่น แค่เปลี่ยนการตั้งค่าเดียว FPS ก็ตกลงอย่างมาก
  • โค้ดหลายส่วนยังปรับแต่งประสิทธิภาพได้อีก แต่ผู้เขียนมองว่าก่อนแก้ปัญหา simplification ได้ การ optimize เหล่านั้นยังไม่ใช่ประเด็นสำคัญ

ความแตกต่างจาก UE5 Nanite

  • ใช้ error metric แบบ projected simplification error ที่เรียบง่าย
  • meshlet simplification ใช้วิธีที่ค่อนข้างง่าย
  • ไม่มี two-pass occlusion culling
    • จะเพิ่มก็ไม่ยาก แต่ทำให้การ debug ยุ่งยากและมีปฏิสัมพันธ์กับการตั้งค่า GUI
    • จึงไม่สอดคล้องกับเป้าหมายด้านความอ่านง่ายของโค้ด
  • ไม่มี visibility buffer
    • ทำไม่ได้เพราะข้อจำกัดของ atomic<u64>
  • ไม่มี built-in shadows และ multiview
  • ไม่มี work queue ภายใน shader
    • สำหรับ meshlet culling และ LOD selection จะ dispatch thread ต่อ meshlet
  • ไม่มีการ eviction และ streaming ของ LOD ที่ไม่ได้ใช้งานออกจาก VRAM
  • ไม่มี compression
  • ขยายไปยังวัตถุที่แตกต่างกันจำนวนมากได้ไม่ดีนัก
    • โฟกัสที่การใช้หน่วยความจำที่คาดการณ์ได้ในเคสเดโม
    • ต้องรู้เพดานบนของ drawn meshlets ล่วงหน้า เพื่อเตรียม buffer สำหรับข้อมูลระหว่าง stage
  • ไม่มี BVH หรือโครงสร้างแบบลำดับชั้นสำหรับ instance
    • ทำ frustum และ occlusion culling กับทุก instance

เงื่อนไขที่จำนวนสามเหลี่ยมสื่อความหมาย

  • แค่ตัวเลข “หลายหมื่นล้าน triangles” อย่างเดียวตัดสินประสิทธิภาพได้ยาก และประสิทธิภาพขึ้นกับหลายปัจจัย
  • Dense meshes

    • ถ้ามี dense mesh จำนวนมากอยู่ใกล้กล้อง อาจส่งผลลบต่อประสิทธิภาพ
    • แต่ถ้าอยู่ใกล้จนกินพื้นที่บนหน้าจอมาก occlusion culling ก็จะเริ่มทำงาน
    • geometry ที่หนาแน่นทำให้ meshlet มีขนาดเล็ก กินพื้นที่บนหน้าจอน้อย และถูก occlusion/cone culling ได้ง่าย
  • Instance count

    • แต่ละ instance มี transform matrix แบบ mat4x3 จึงใช้ VRAM
    • ระหว่างเฟรมยังต้องเก็บรายการสิ่งที่จะ render ด้วย
    • ในกรณีเลวร้ายที่สุด แต่ละ instance จะ render meshlet ที่หนาแน่นที่สุด
    • ใน implementation นี้จะจัดสรร instanceCount * bottomLevelMeshletsCount * sizeof(vec2u) bytes
    • ขีดจำกัดเริ่มต้นของ WebGPU storage buffer ใน Chrome คือ 128MB และฉากเดโมถูกปรับให้สอดคล้องกับข้อจำกัดนี้
  • Scene arrangement

    • ฉากในแอปจะจัดวางวัตถุเป็นรูปสี่เหลี่ยมจัตุรัส
    • วัตถุที่ไกลจะมองเห็นได้เพียงบางส่วน แต่ coarse meshlet LOD ครอบคลุมพื้นที่กว้างกว่า ทำให้เกิด overdraw
    • การวางแบบ dense grid ทำให้ render สามเหลี่ยมใกล้กล้องมากขึ้น แต่เพราะความต่างของความลึกน้อย จึงเป็นผลดีกับ occlusion culling
    • วัตถุที่อยู่ห่างกันมากอาจทำให้พิกเซลไกลแบบสุ่มไปปนเปื้อน depth pyramid ได้

ทำไมฉาก Jinx ถึงยาก

  • ตัวละคร Jinx มีรูปร่างผอมและมีช่องว่างระหว่างแขนกับลำตัว ทำให้ occlusion culling ยาก
  • โมเดลถูกทำให้เรียบง่ายได้ไม่ดี ทำให้แม้แต่ coarse LOD ที่สุดก็ยังมี 3k triangles
  • เกิดสถานการณ์ที่สามเหลี่ยมขนาด 1 พิกเซลจำนวนมากสะสมกัน
  • ซอฟต์แวร์แรสเตอร์ไรเซอร์ช่วยได้มาก แต่เพราะการจัดวางฉาก instance ส่วนใหญ่จึงถูก render เป็น impostor
  • ในบริเวณใกล้กล้องจะเป็นหน้าที่ของ hardware rasterizer และทั้ง hardware rasterizer, software rasterizer และ impostor ต่างก็มีจุดแข็งคนละแบบ

จุดที่น่าประหลาดใจใน implementation ของ Nanite

  • เป้าหมายของ DAG ไม่ใช่แค่ใช้สามเหลี่ยมน้อยลงกับวัตถุไกล แต่คือการรักษาความสม่ำเสมอให้ใกล้เคียง 1 pixel == 1 triangle ทั่วทั้งหน้าจอ
  • discrete LOD mesh มี geometry แยกกันในแต่ละ LOD จึงรับมือได้ยากเมื่อจำเป็นต้องมีระดับกึ่งกลาง และต้องใช้ continuous LOD
  • meshlet hierarchy ช่วยให้ sample geometry ได้ตามระดับรายละเอียดที่เลือก
  • เวลาส่วนใหญ่หมดไปกับ culling และงานเกี่ยวกับ meshlet มากกว่าตัว Nanite เอง
  • meshlet LOD hierarchy ทำให้พอใช้งานได้ไม่ยาก แต่ทำให้มีประสิทธิภาพนั้นยาก
  • หาก mesh ถูก simplification ได้ไม่สะอาด ก็จะเกิดกรณีแบบ Jinx ที่มีประมาณ 3,000 triangles มาทับพิกเซลเดียว
  • ถ้าต้องการ triangles ขนาดระดับพิกเซล ก็จำเป็นต้องมีซอฟต์แวร์แรสเตอร์ไรเซอร์ และ billboard impostor ก็มีประโยชน์ในฐานะ fallback ที่เน้นความเสถียร

ปัญหา Mesh simplification

  • simplification ไม่ใช่ปัญหาแบบ “รับ mesh มาแล้วลดให้เหลือ X% triangles” แต่เป็นปัญหาที่ต้องทำในบริบทของ meshlet และ METIS
  • UE5 ใช้ mesh simplification code ของตัวเองเป็นขั้นตอนแรกใน asset pipeline
    • ต้นทุนที่ลดได้ตรงนี้จะส่งผลดีต่อเนื่องไปทั้งระบบถัดจากนั้น
    • ในเอกสาร SIGGRAPH ของ Brian Karis ระบุว่า LOD graph ลงท้ายด้วย root cluster เดียว และทุกโมเดลสามารถ simplification ลงไปถึง 128 triangles ได้
  • โมเดล Jinx มีปัญหาที่ simplification หยุดค้างอยู่ที่จุดหนึ่ง
    • ใส่ X triangles เข้าไป แต่ได้ X triangles เดิมกลับมา
    • อัลกอริทึม crash ด้วย assertion
    • หลังจากนั้นจึงเปลี่ยนให้ส่วน mesh ที่ลดลงได้ไม่พอสามารถมีหลาย root ใน DAG ได้
  • ตอนแรกโมเดล bunny ถูก simplification เป็น meshlet เดียวขนาด 128 tris แต่เมื่อเปลี่ยนเงื่อนไขก็พบปัญหาว่าไม่ลงไปถึง single root
    • meshlet จำนวนมากลดจำนวน triangle ได้ไม่มากพอ และยังมี meshlet ที่ “ไม่เต็ม” จำนวนมากซึ่งมีน้อยกว่า 128 triangles
  • ประเด็นที่เกี่ยวข้อง:

เมตริกข้อผิดพลาด

  • เมื่อเรนเดอร์เมช 20,000,000 triangles เป็นลำดับชั้น meshlet จำเป็นต้องเลือกให้ได้ว่า meshlet ใดคือ meshlet ที่ “ถูกต้อง”
  • ปัญหาการเลือกนี้คือแก่นหลักของ Nanite และ simplification, meshlet DAG, software rasterizer ก็แทบจะเป็นเงื่อนไขเบื้องต้นเพื่อใช้จัดการปัญหานี้
  • การติดตั้งใช้งานนี้ใช้ projected simplification error ที่ meshoptimizer มีให้
  • ผู้เขียนมองว่า metric นี้ไม่ใช่ metric ที่ดีสำหรับ Nanite และควรใส่ vertex attribute อื่น ๆ เข้าไปในฟังก์ชันด้วย
    • ควรกำหนด weight แยกตามแต่ละ attribute ได้
    • normal ของใบหน้า Jinx เป็นปัญหาใหญ่
  • เอกสารอ้างอิง:

เหตุผลที่ software rasterizer ไม่เขียน texture ออกมา

  • ใน hardware rasterizer สามารถเขียน depth texture, color และ normal ไปพร้อมกันได้ตามผลของ depth test
  • ใน software rasterizer หากหลาย thread เขียนลงพิกเซลเดียวกัน จะเกิด race condition
  • วิธีแก้โดยทั่วไปคือ visibility buffer
    • เขียน sceneUniqueTriangleId ของ triangle ที่อยู่ใกล้ที่สุดลงในแต่ละ pixel
    • นำค่านี้ไปรวมกับ depth 32-bit ให้เป็นค่า 64-bit และใช้ 64-bit atomic operation
    • จากนั้นใน pass แยกต่างหาก จะ rasterize triangle อีกครั้ง คำนวณ barycentric coordinates แล้วทำ shading
  • WebGPU ไม่มี 64-bit atomics จึงใช้วิธีนี้ไม่ได้
  • เป้าหมายของโปรเจกต์นี้คือการแสดงให้เห็นว่า software rasterize สามารถทำงานได้ ดังนั้นโมเดลที่ software rasterize ในพื้นหลังจึงเป็นสีขาวและให้เพียง shading ในระดับที่สมเหตุสมผล

มุมมองต่อการทำ Nanite ขึ้นเอง

  • คำตอบที่ง่ายที่สุดคือใช้ UE5
  • หากต้องการใส่เทคโนโลยีนี้ลงในเอนจินที่มีอยู่ ผู้เขียนมองว่าต้องเริ่มจากการทำ compute-based graphics pipeline และ GPU-driven rendering pipeline ก่อน
  • จากนั้นปัญหาอย่าง multi-step culling, scene/world chunk management และ animated meshes จะตามมาก่อน
  • หลังจากทำให้ขั้นตอนเหล่านี้เสถียรแล้ว ค่อยลองทำ software rasterizer และค่อยเพิ่มเทคโนโลยีแบบ Nanite ต่อเป็นลำดับ
  • ใน toy renderer การทำ meshlet hierarchy พื้นฐานอาจเป็นโปรเจกต์ช่วงสุดสัปดาห์ได้ แต่ของจริงต้องรับมือกับปัญหา simplification และ error metric

เครื่องมือและแหล่งข้อมูลอ้างอิงที่ใช้

  • meshoptimizer คือฐานหลักสำคัญของการติดตั้งใช้งานโปรเจกต์นี้
    • meshoptimizer เวอร์ชันใหม่กว่าจะมี meshopt_SimplifySparse สำหรับทำ Nanite clone
    • โปรเจกต์นี้ไม่ได้อัปเดตไปใช้เวอร์ชันนั้น เพื่อคงสถานะที่ใช้ทดสอบระหว่างการพัฒนาไว้
  • ใช้ METIS
  • Emscripten ถูกใช้เพื่อรัน meshoptimizer และ METIS บนเบราว์เซอร์
  • โมเดล Jinx ใช้ Arcane - Jinx จาก Sketchfab และมีการรวม texture, ปรับ UV และนำอาวุธออก

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น