Skip to content

पहाड़ी के पीछे की घास रेंडर न करें: भूभाग-सजग ऑक्लूज़न कलिंग

लेखक: Oleg Sidorkin, Cinevva के CTO और सह-संस्थापक

भाग 28 में Spike 57 पेश किया गया था और कुछ अनुच्छेदों में उसके चार कलिंग पाथ का बेंचमार्क किया गया था। यह उसका विस्तृत संस्करण है: वही चार क्यों चुने गए, बाकी चार कौन-से थे, और जिसे हम रिलीज़ कर रहे हैं उसके पीछे क्या तर्क है।

कैमरे और एक घास के मैदान के बीच एक पहाड़ी है। आपको उस मैदान को रेंडर करने की ज़रूरत नहीं है। हर आधुनिक AAA इंजन यह जानता है। अधिकतर ब्राउज़र-आधारित 3D इंजन नहीं जानते—पिछले सप्ताह तक हमारा इंजन भी नहीं जानता था।

यह पोस्ट शोध को ठीक से करने, उसे हमारे वास्तविक कोडबेस के संदर्भ में समझने, एक काम करने वाला स्पाइक बनाने, और तकनीकों को इस आधार पर आँकने का परिणाम है कि वे विशेष रूप से हमारी कितनी मदद करती हैं—यह देखते हुए कि आज हम WebGL पर और कल WebGPU पर रिलीज़ करते हैं।

आगे पढ़ने से पहले आप नीचे लाइव स्पाइक आज़मा सकते हैं। T/Y/U/I से कलिंग पाथ बदले जाते हैं, C से कैमरा प्रीसेट क्रमशः बदलते हैं, और B छिपाए जाने वाले चंक्स को लाल वायरफ़्रेम में रंग देता है, ताकि आप देख सकें कि परीक्षण क्या हटाता है। HUD बताता है कि प्रत्येक चरण कितने इंस्टेंस अस्वीकार करता है।

Spike 57 को नए टैब में खोलें ↗ · सोर्स देखें

हमारा इंजन आज क्या करता है (और क्या नहीं करता)

हमारा स्ट्रीमिंग चंक मैनेजर खिलाड़ी के चारों ओर 64m के चंक्स की एक रिंग को तीन LOD स्तरों पर लोड करता है। प्रत्येक चंक में हाइटमैप पर बिखेरे गए इंस्टेंस्ड पेड़ होते हैं। Chunk.updateObjectVisibility में मौजूद कलिंग लॉजिक केवल दूरी की जाँच करता है: 60m के भीतर के पेड़ पूरे मेश के रूप में रेंडर होते हैं, 60m से 140m के बीच बिलबोर्ड के रूप में, और उससे आगे कुछ भी नहीं।

यह कलिंग बेकार होने वाले काम की दो विशाल श्रेणियाँ छोड़ देती है:

  1. 60m के दायरे के भीतर मौजूद हर चीज़ जो कैमरे के पीछे है। जब भी खिलाड़ी 4m से अधिक चलता है, हम इंस्टेंस बफ़र को फिर से सॉर्ट करते हैं, लेकिन व्यू फ्रस्टम की जाँच नहीं करते। इसलिए औसतन आधे दायरे का डेटा GPU को भेजा जाता है, केवल इसलिए कि वर्टेक्स ट्रांसफ़ॉर्म के बाद उसे क्लिप कर दिया जाए।
  2. 60m के दायरे के भीतर मौजूद हर चीज़ जो किसी पहाड़ी के पीछे है। हमारे भूभाग की ऊर्ध्वाधर रेंज 80m है और इसमें बहुत-सी घाटियाँ हैं। जब कैमरा किसी घाटी में होता है, तो कलिंग त्रिज्या के भीतर की अधिकतर वनस्पति निकटतम पहाड़ी धार से ज्यामितीय रूप से छिपी होती है। फिर भी हम उसे ड्रॉ करते हैं।

यह पोस्ट दूसरी श्रेणी पर केंद्रित है। यही वह श्रेणी भी है जिसे AAA इंजन कई चतुर तकनीकों की परतों से ठीक करते हैं, जो पहली नज़र में ब्राउज़र के लिए आसानी से उपयुक्त नहीं लगतीं।

Guerrilla के Gilbert Sanders बताते हैं कि Horizon Zero Dawn ओपन-वर्ल्ड वनस्पति को कैसे रेंडर करता है। अंतिम 20 मिनट कलिंग स्टैक पर हैं और यह समझने की उत्कृष्ट कक्षा है कि "कम ड्रॉ करना", "तेज़ ड्रॉ करने" से बेहतर क्यों है।

आठ तकनीकें, हमारे उपयोग के लिए मूल्यांकित

मैंने आधुनिक ऑक्लूज़न-कलिंग स्टैक का अध्ययन किया और प्रत्येक हिस्से को इस आधार पर आँका कि वह प्रक्रियात्मक, हाइटमैप-आधारित, ब्राउज़र में चलने वाले ओपन वर्ल्ड के लिए कितना उपयोगी है। इसके दो पैमाने हैं: मूल्य (हमारे दृश्य में यह कितना व्यर्थ काम हटाता है) और लागत (इंजीनियरिंग प्रयास, साथ ही हमारे स्टैक के कितने हिस्से को बदलना पड़ेगा)। पूरा तर्क इस पोस्ट के पीछे मौजूद शोध फ़ाइल में है; यहाँ उसका संक्षिप्त संस्करण दिया गया है।

1. प्रति-इंस्टेंस फ्रस्टम कलिंग। उच्च मूल्य, कम लागत। अभी हम अपनी इंस्टेंस्ड वनस्पति के लिए फ्रस्टम की जाँच बिल्कुल नहीं करते। इसे जोड़ने से किसी भी व्यू में लगभग आधा काम घट जाता है, करीब तीस पंक्तियों का कोड लगता है, और इसे आज ही WebGL में रिलीज़ किया जा सकता है। यह कम मेहनत में मिलने वाला सबसे बड़ा लाभ है और हमें इसे महीनों पहले कर लेना चाहिए था।

2. हाइटमैप होराइज़न रेकास्ट। उच्च मूल्य, कम–मध्यम लागत। कैमरे से प्रत्येक संभावित इंस्टेंस की ओर एक रे आगे बढ़ाएँ और रास्ते में भूभाग की ऊँचाई का सैंपल लें। यदि भूभाग कभी रे से ऊपर उठता है, तो इंस्टेंस ऑक्लूडेड है। यह ठीक इसलिए काम करता है क्योंकि हमारा वर्ल्ड एक हाइटमैप है, जिससे दृश्यता क्षैतिज दिशा में एक 1D समस्या बन जाती है। सघन संस्करण की लागत प्रति इंस्टेंस, प्रति फ़्रेम O(steps) है। त्वरित संस्करण (अगला बिंदु) इसे घटाकर O(log steps) कर देता है।

3. मैक्स-हाइट पिरामिड एक्सेलेरेशन। उच्च मूल्य, मध्यम लागत। हाइटमैप का एक 2D मिपमैप, जिसमें प्रत्येक टेक्सेल अपने क्षेत्र के भीतर भूभाग की अधिकतम ऊँचाई संग्रहीत करता है। इससे होराइज़न रेकास्ट समतल परिवेश में बड़े चरणों में आगे बढ़ सकता है और केवल पहाड़ियों के ऊपर सूक्ष्म जाँच करता है। यही संरचना #2 को प्रोडक्शन के लिए सस्ता बनाती है।

4. हायरार्किकल-Z ऑक्लूज़न (Hi-Z / HZB)। उच्च मूल्य, उच्च लागत। डेप्थ-बफ़र का मिपमैप बनाएँ, प्रत्येक इंस्टेंस की सीमाओं को स्क्रीन स्पेस में प्रोजेक्ट करें और सही मिप स्तर के विरुद्ध जाँचें। यह आधुनिक GPU-साइड मानक है, जिसका उपयोग Unreal का Nanite, Bevy की वर्चुअल ज्योमेट्री और VTK का WebGPU पोर्ट करते हैं। यह केवल भूभाग के लिए नहीं, बल्कि हर चीज़ के लिए काम करता है, लेकिन इसके लिए WebGPU, इनडायरेक्ट ड्रॉ और एक कंप्यूट पास चाहिए। यह तब लाभकारी होता है जब हम हज़ारों पेड़ों के बजाय घास की लाखों पत्तियाँ रेंडर कर रहे हों।

5. टू-पास Hi-Z (Nanite-शैली)। #4 की तुलना में मामूली लाभ, उच्च लागत। एक फ़्रेम वाले डिसऑक्लूज़न आर्टिफ़ैक्ट से बचने के लिए पास 1 के बाद इस फ़्रेम की डेप्थ को दोबारा रेंडर करें। यह तभी उपयोगी है जब हम GPU-चालित पाथ पर इतने आगे पहुँच जाएँ कि इसकी लागत केवल वृद्धिशील हो।

6. सॉफ़्टवेयर ऑक्लूज़न रैस्टराइज़र (Frostbite/Intel MOC)। मध्यम मूल्य, उच्च लागत। CPU पर बड़े ऑक्लूडर्स का कम-रिज़ॉल्यूशन डेप्थ बफ़र रैस्टराइज़ करें। रीडबैक विलंबता शून्य। संदर्भ कार्यान्वयन AVX/SSE C++ में हैं; उन्हें WASM में पोर्ट करना अपने आप में एक वास्तविक प्रोजेक्ट है, जबकि हमारा हाइटमैप रेकास्ट बहुत कम मेहनत में लगभग वही लाभ दे देता है।

7. प्रीकंप्यूटेड PVS। कम मूल्य, उच्च लागत। Quake-युग के स्थिर मैप्स के लिए शानदार। हमारा भूभाग प्रक्रियात्मक और अनंत है, इसलिए कोई भी प्रीप्रोसेसिंग चंक स्ट्रीम होने के समय करनी पड़ेगी, जिसकी लागत लगभग रनटाइम विज़िबिलिटी जितनी ही होगी। छोड़ दें।

8. Cesium-शैली होराइज़न कलिंग। हमारे लिए कोई मूल्य नहीं, मध्यम लागत। ग्रहों के दीर्घवृत्ताभों के लिए डिज़ाइन की गई। हमारा वर्ल्ड लगभग समतल और सीमित है; यह गणित लागू नहीं होता और या तो कुछ नहीं करेगा या गलत तरीके से कलिंग करेगा। छोड़ दें।

इसलिए: #1 और #2+#3 को अभी CPU पर WebGL में करें। WebGPU माइग्रेशन के लिए #4 की योजना बनाएँ। बाकी को छोड़ दें।

ब्राउज़र भूभाग के लिए हाइटमैप रेकास्ट क्यों जीतता है

किसी भी आधुनिक रेंडरिंग वार्ता में मानक सलाह होती है: "Hi-Z बफ़र बनाएँ।" Nanite पर Brian Karis का SIGGRAPH 2021 का गहन विवरण इसका प्रामाणिक संदर्भ है और आपको इसे कम-से-कम एक बार देखना चाहिए।

यह ऐसे इंजन के लिए सही उत्तर है जो पहले से ही हर चीज़ को GPU-चालित इनडायरेक्ट ड्रॉ के माध्यम से चलाता है। हमारे इंजन सहित अधिकतर ब्राउज़र इंजन ऐसे नहीं हैं। हमारे पास CPU-साइड इंस्टेंस बफ़र, WebGL ड्रॉ कॉल और कोई कंप्यूट चरण नहीं है। उस स्टैक में Hi-Z जोड़ने का अर्थ है एक साथ WebGPU पर पोर्ट करना, वनस्पति पाइपलाइन को इनडायरेक्ट ड्रॉ के रूप में फिर से लिखना, और डेप्थ-पिरामिड बिल्ड पास जोड़ना। विचार को सिद्ध करने वाला पहला फ़्रेम पाने में ही तीन महीने लग जाएँगे।

हाइटमैप रेकास्ट CPU पर, WebGL में और हमारे पास पहले से मौजूद डेटा के साथ काम करता है। यह हमारे वर्ल्ड के उस एक तथ्य का उपयोग करता है जिसका लाभ AAA इंजन नहीं उठा सकते: हमारे ऑक्लूडर्स को एक 1D ऊँचाई फ़ंक्शन से वर्णित किया जाता है। किसी रे के साथ उस फ़ंक्शन का सैंपल लेने में दो ऐरे लुकअप और एक गुणा लगता है। Hi-Z बफ़र को यही तथ्य पिक्सेल-दर-पिक्सेल खोजना पड़ता।

यह तकनीक पहली बार IEEE Visualization 2002 में "Horizon Occlusion Culling for Hierarchical Terrains" के रूप में प्रकाशित हुई थी (PDF)। यह दो दशकों से टूलबॉक्स में बनी हुई है क्योंकि इसकी प्रकृति सही है: भूभाग समतल हो तो सस्ती, केवल वास्तविक पहाड़ियों पर महँगी, और आसानी से समानांतर की जा सकने वाली।

मैक्स-हाइट पिरामिड का चित्रण

सरल रेकास्ट प्रत्येक रे के साथ करीब 24 बिंदुओं पर terrainHeight का सैंपल लेता है और भूभाग के रे को पार करते ही जल्दी रुक जाता है। हज़ारों पेड़ों के लिए यह ठीक है। घास की लाखों पत्तियों पर यह ढह जाता है।

समाधान हाइटमैप का ऐसा मिपमैप है जिसमें प्रत्येक टेक्सेल अपने क्षेत्र के भीतर की अधिकतम ऊँचाई संग्रहीत करता है:

level 0 (256×256, 2.25m per texel):  max h of 4×4 jittered samples
level 1 (128×128, 4.5m  per texel):  max(0,0), max(1,0), max(0,1), max(1,1)
level 2 ( 64×64,  9.0m per texel):  same reduction one level up
...
level 8 (   1×1,  576m per texel):  global max

जब रे खंड लंबा और समतल हो, तो मोटे स्तर का सैंपल लें: एक लुकअप आपको बता देता है, "इस 9m वर्ग में कोई भी भूभाग कभी 12m की ऊँचाई से ऊपर नहीं जाता और रे वहाँ 30m पर है, इसलिए आगे बढ़ें।" केवल तभी एक स्तर नीचे जाकर अधिक सूक्ष्म जाँच की जाती है जब कोई मोटा टेक्सेल कहता है, "भूभाग रे से ऊपर हो सकता है।" पूरी संरचना केवल कुछ सौ KB की होती है और कुछ दर्जन मिलीसेकंड में बन जाती है।

रेखाचित्र के रूप में:

                                            ray from eye
        eye 1.7m                          o─────────────────────►
              o─────────────────────────·─·─·─·─·─·──────────────
              │                          \                       │
              │   level 3 (huge step)     \   level 0 (refine)   │
              │   "no terrain above 8m"    \   "9m hill here!"   │
              │                              \                   │
        ──────┴────────────/▔▔▔\─────────────/▔▔▔▔▔\─────────────
                                 hill A (8m)   hill B (12m)

                                                blocks here

पहाड़ी A के आसपास से गुज़रने वाले रे खंड के लिए स्तर-3 लुकअप ("इस 18m चौड़े बॉक्स में अधिकतम ऊँचाई 8m है") हमें पहले ही बता देता है कि 1.7m की ऊँचाई और कुछ मीटर की चढ़ाई पर मौजूद रे का रास्ता साफ़ है। हम एक क्वेरी में 36m की मार्चिंग छोड़ देते हैं। पहाड़ी B पर स्तर 3 कहता है, "यहाँ अधिकतम ऊँचाई 12m है"; हम नीचे उतरते हैं, स्तर 0 कहता है, "हाँ, इसी टेक्सेल पर 12m", और हम इंस्टेंस को अस्वीकार कर देते हैं।

पिरामिड का निर्माण height-pyramid.mjs में है और उसका उपयोग करने वाले कलिंग पाथ cull.mjs में हैं। दोनों को स्पाइक सोर्स ब्राउज़र में आसानी से पढ़ा जा सकता है।

स्पाइक वास्तव में क्या दिखाता है

ऊपर Spike 57 खोलें और चारों पाथ आज़माएँ:

  • T0 वही है जो प्रोडक्शन आज करता है। केवल दूरी। C1 (घाटी की तलहटी) से HUD करीब 12,000 दृश्यमान घास की पत्तियाँ दिखाता है।
  • T1 प्रति-इंस्टेंस फ्रस्टम कलिंग जोड़ता है। दृश्यमान संख्या लगभग आधी रह जाती है, क्योंकि कैमरे के पीछे या किनारे से बाहर की हर चीज़ GPU तक पहुँचने से पहले हटा दी जाती है।
  • T2 ब्रूट-फ़ोर्स हाइटमैप रेकास्ट जोड़ता है। C1 में संख्या और 60-80% घट जाती है, क्योंकि मैदान का अधिकतर हिस्सा निकटतम पहाड़ी धार के दूसरी ओर है। Cull-ms कॉलम बढ़ता है क्योंकि हम प्रति इंस्टेंस करीब 24 बार terrainHeight का सैंपल ले रहे हैं।
  • T3 ब्रूट फ़ोर्स की जगह मैक्स-हाइट पिरामिड का उपयोग करता है। दृश्यता का लाभ बनाए रखते हुए Cull-ms फिर T1 के करीब आ जाता है। यही वह पाथ है जिसे आप वास्तव में रिलीज़ करेंगे।

यह पैटर्न प्रोडक्शन इंजनों में दिखने वाले परिणामों से मेल खाता है। घास रेंडरिंग पर Acerola की दो-भाग वाली शृंखला (गेम्स इतनी घास कैसे रेंडर करते हैं?, मैंने अपने गेम की घास को ऑप्टिमाइज़ करने के लिए क्या किया) YouTube पर उपलब्ध सबसे सुलभ व्याख्या है कि इंजीनियरिंग प्रयास कलिंग चरण पर क्यों खर्च करना चाहिए, शेडिंग चरण पर नहीं।

कैमरा C3 और "पहाड़ी की चोटी" वाला विफलता मामला

स्पाइक का तीसरा कैमरा प्रीसेट कैमरे को एक पहाड़ी के ऊपर रखता है, जहाँ से वह पूरे खेल क्षेत्र की ओर देखता है। यह वह स्थिति है जहाँ होराइज़न कलिंग लगभग कुछ नहीं करती, क्योंकि कैमरे और वर्ल्ड के अधिकतर हिस्से के बीच कोई भूभाग नहीं है। HUD दिखाता है कि T1 की तुलना में T2/T3 दृश्यमान संख्या को शायद केवल 5-10% कम करते हैं।

यह बग नहीं, विशेषता है। तकनीक ठीक वहीं काम करना बंद करती है जहाँ उसे करना चाहिए: जब ऑक्लूड करने के लिए कुछ है ही नहीं। फ्रस्टम कलिंग अब भी वास्तविक काम कर रही है, दूरी-आधारित कलिंग अब भी बजट को सीमित कर रही है, और होराइज़न परीक्षण सहजता से नो-ऑप बन जाता है। यदि आप इसे लागू करते हैं तो आपको यह भी सत्यापित करना होगा कि नो-ऑप स्थिति सस्ती है। इसी कारण मैक्स-हाइट पिरामिड तब भी मायने रखता है जब कोई रे अस्वीकार न हो: सबसे मोटा स्तर अक्सर यह पुष्टि करने के लिए पर्याप्त होता है कि "कुछ भी अवरुद्ध नहीं है।"

हम आगे क्या रिलीज़ करेंगे

क्रम से तीन काम करने हैं।

पहला, T1 और T3 को प्रोडक्शन के Chunk.updateObjectVisibility में पोर्ट करें। पिरामिड को एक स्तर ऊपर, चंक मैनेजर पर रहना चाहिए, क्योंकि यह एक से अधिक चंक में फैला होता है। कलिंग Chunk में ही रहेगी, ताकि मौजूदा प्रति-चंक बैचिंग काम करती रहे। अनुमानित प्रयास: परीक्षणों सहित एक दिन। दूसरा, घास उपलब्ध होने पर उसके लिए भी यही करें। मौजूदा स्पाइक 576m के खेल-क्षेत्र में 12,000 ब्लेड बिखेरता है; प्रोडक्शन घनत्व इससे लगभग दस गुना अधिक होना चाहिए। CPU कल पाथ 12,000 ब्लेड को एक मिलीसेकंड से कम समय में संभाल लेते हैं, और पिरामिड एक्सेलरेशन ही 120,000 ब्लेड पर भी इसे संभव बनाए रखता है।

तीसरा, जब हम THREE.WebGPURenderer पर माइग्रेट करें, तो इसी लूप को कंप्यूट शेडर में पोर्ट करें। मेटाडेटा एक स्टोरेज बफ़र बन जाता है। कल प्रक्रिया drawIndirect आर्ग्युमेंट लिखती है। पिरामिड को एक 2D टेक्सचर के रूप में अपलोड किया जाता है, जिसमें मैक्स-रिडक्शन पहले से बेक होता है। कोड की संरचना लगभग समान रहती है, और यही इसका मूल उद्देश्य है: हम माइग्रेशन के लिए किसी नए एल्गोरिदम पर दाँव नहीं लगा रहे हैं, बल्कि अपने मौजूदा एल्गोरिदम को एक तेज़ रनवे पर ले जा रहे हैं।

Guerrilla ने Horizon Zero Dawn के प्रोसीजरल प्लेसमेंट सिस्टम के लिए इसके GPU-साइड संस्करण को प्रस्तुत किया था; रेंडरिंग पाइपलाइन की तुलना में डेटा स्ट्रक्चर अधिक महत्वपूर्ण हैं, और उनकी संरचना भी ऐसी ही है:

जब हम इमारतें और सघन प्रॉप्स शिप करेंगे, जो उन दिशाओं में ऑक्लूड करते हैं जिन्हें हाइटमैप व्यक्त नहीं कर सकता, तब भी Hi-Z अपनी उपयोगिता साबित करेगा। लेकिन हाइटमैप पिरामिड पाइपलाइन में बना रहेगा, क्योंकि ज़मीन से सटे इंस्टेंस के लिए यह Hi-Z से स्पष्ट रूप से सस्ता है, और किसी रिज की सिल्हूट को लाँघती घास ठीक वही स्थिति है जिसे Hi-Z सबसे खराब ढंग से संभालता है।

संदर्भ

पूरी रैंक की गई तर्क-व्याख्या और आर्किटेक्चरल निर्णय ऊपर दिए गए हैं; यहाँ प्रत्येक तकनीक के प्रामाणिक स्रोत लगभग उसी क्रम में दिए गए हैं, जिस क्रम में वे स्टैक में दिखाई देते हैं।