Skip to content

Browser में open world बनाना, भाग 19: वह imposter जिसे एक जंगल में टिके रहना है

लिखा है Oleg Sidorkin ने, Cinevva के CTO और Co-Founder

यहाँ नए हैं? series guide देखें। यह समझाती है कि spike क्या होता है और सभी भागों को लिंक करती है।

भाग 18 ने creators को एक brush दिया जो किसी पहाड़ी को पेड़ों से भर देता है। पेच यह है कि जब स्क्रीन पर हज़ारों पेड़ हों तो उनकी कीमत क्या होती है। एक दूर का पेड़ चार pixel देने के लिए 2,000 triangles की ज़रूरत नहीं रखता। यह भाग सबसे गहरा LOD है: imposter, एक चपटा quad जो पेड़ की एक तस्वीर पहने हुए है, और "एक पेड़ जो सही दिखता है" से लेकर GPU पर उनके दस लाख तक का रास्ता।

एक पेड़ billboard पर दो textures है

Spike 38 को नए tab में खोलें ↗ · Source देखें

एक imposter किसी prop को देखने के कोणों के एक grid से दो texture atlases में पहले से render कर देता है, एक color के लिए और एक world-space normals के लिए, फिर runtime पर एक ही camera-facing quad दिखाता है जो वह tile sample करता है जो मौजूदा view से मेल खाती है। Bake प्रति tile दो passes का होता है: diffuse एक unlit material के साथ ताकि texture में कोई lighting bake न हो, और normals को normalWorld × 0.5 + 0.5 के रूप में encode किया जाता है जिसमें alpha source से आगे भेजा जाता है ताकि silhouette pixel-दर-pixel मेल खाए। Runtime material एक पूरा MeshStandardNodeMaterial है, इसलिए imposter अब भी किसी भी दूसरी surface की तरह scene का sun और IBL लेता है। फायदा geometry में है: हज़ारों triangles के बजाय एक quad, और detail एक 1 MB texture में रहती है।

इसे अपने अलग spike में खींच लाना अपने आप में एक सबक था। Imposter spike 37 के scatter system के अंदर सबसे गहरे LOD के रूप में शुरू हुआ था, और bake पर हर iteration को पूरी scatter pipeline के ज़रिए test करना पड़ता था, जहाँ bake की सही-गलती instance-matrix migration और LOD swaps में उलझी रहती थी। इसे एक prop, एक quad तक अलग करके, original के साथ कंधे से कंधा मिलाकर रखने से iteration का समय मिनटों से सेकंडों तक गिर गया।

जब किताबी जवाब ही गलत जवाब हो

पहले implementation में octahedral encoding इस्तेमाल हुई, जो sphere directions को एक square में pack करने की किताबी mapping है। इसने numerical roundtrip tests पास किए, और फिर भी user बार-बार imposter के ऐसे tile पर snap हो जाने के screenshot भेजता रहा जो पेड़ को सामने से देखने के बजाय थोड़ा ऊपर से देखता हुआ लगता था। छह दौर की fixes आईं (एक per-quad view-direction uniform, एक square bake aspect, debug materials, static billboarding) और हर एक सचमुच ज़रूरी थी पर कोई भी असली bug नहीं था। Fix तभी आया जब "इसे शुरू से फिर सोचो, KISS, कोई band-aid नहीं।"

Rewrite ने octahedral folding को फेंक दिया और सादे azimuth बनाम elevation को अपनाया: az = atan2(dir.x, dir.z), el = asin(dir.y), uv = (az/2π, el/π + ½)। यही पूरी encoding है, कोई L1 normalize नहीं, sign-of-zero के कोई corner cases नहीं। यह यहाँ बेहतर इसलिए नहीं है कि यह ज़्यादा सटीक है (यह एक कम uniform sphere sample करता है), बल्कि इसलिए कि CPU cell-picker और GPU shader एक ही primitives इस्तेमाल करते हैं, इसलिए वे किसी boundary direction पर उस तरह असहमत नहीं हो सकते जैसे octahedral जोड़ी चुपचाप हो जाती थी। Atlas एक contact sheet की तरह पढ़ा जाता है: column है prop के चारों ओर का कोण, row है elevation, overlay में एक नज़र में साफ दिखता है।

इसके बाद भी "थोड़ा ऊपर से देखता है" वाली शिकायत बनी रही, और इसकी वजह एक quantization चुनाव थी, encoding नहीं। एक 4×4 grid के साथ row centers ±22.5° और ±67.5° पर आते हैं, इसलिए ठीक 0° elevation पर कोई row नहीं होती। क्षैतिज देखने वाला viewer, जो कहीं ज़्यादा आम मामला है, हमेशा किसी tilt पर bake की गई row में गिरता है। Fix है odd N: एक 5×5 grid row centers को 0° और ±36° और ±72° पर रखता है, इसलिए क्षैतिज viewer को ठीक क्षैतिज पर bake की गई tile मिलती है। "आधे cell जितनी चूक" वाली गलती का यही परिवार अगले भाग के parallax काम में फिर सामने आता है, और इलाज वही सवाल है: क्या मेरा discrete sample point वाकई वहीं गिरता है जहाँ canonical input के लिए मैं सोचता हूँ?

दो और बातें मायने रखती थीं। Billboard को piecewise-static होना ज़रूरी है, लगातार camera की ओर मुँह करने वाला नहीं। Imposter एक खास bake direction से ली गई चपटी तस्वीर है, इसलिए runtime quad के image plane को उस bake camera के plane से मेल खाना ही चाहिए, जिसका मतलब है कि यह उस arc भर में अपना orientation थामे रखता है जहाँ एक cell चुनी रहती है, फिर boundary पर snap हो जाता है। और budget को वहाँ जाना चाहिए जहाँ players असल में देखते हैं: एक बाद के pass ने tilted top-down rows को पूरी तरह हटाकर उनकी जगह 15° के फासले पर 24 horizontal ring slots और एक सीधी-नीचे वाली tile रख दी, क्योंकि पेड़ करीब-करीब हर वक्त आँख की ऊँचाई से देखे जाते हैं।

Snap, और blending ने उसे कैसे मिटाया

Spike 42 को नए tab में खोलें ↗ · Source देखें

Piecewise-static billboard दूरी पर अदृश्य रहता है और करीब आने पर pop करता है, जो तब तक ठीक है जब तक आप orbit न करें। Spike 42 ने चार variants को साथ-साथ रखा (original prop, az/el 5×5 baseline, और दो hemi-octahedral grids) ताकि flicker को अलग करके खत्म किया जा सके। दो artifacts snap को चलाते हैं। Cell pop इसलिए होता है क्योंकि fragment shader view direction को 25 cells में से एक में quantize कर देता है, इसलिए किसी boundary को पार करना उसी frame पर sample की गई tile बदल देता है और quad को फिर से aim कर देता है। Pole degeneracy है top-down view, जहाँ हर azimuth एक ही point पर ढह जाता है और ring तथा top tile के बीच का पुल atlas का सबसे बुरा transition है।

Hemi-octahedral mapping दोनों को ठीक करती है। यह ऊपरी hemisphere को unit square पर लगातार map करती है, इसलिए पास-पास की 3D directions पास-पास की UVs पर गिरती हैं और कोई pole singularity नहीं रहती और किसी खास top-down tile की ज़रूरत नहीं पड़ती। Flicker का इलाज है bilinear cell blending: सबसे नज़दीकी tile पर snap करने के बजाय, encode की गई direction को घेरने वाला 2×2 tiles का group ढूँढो और चारों को blend करो, कुल मिलाकर 8 texture taps (4 diffuse, 4 normal)। पास-पास के views अब pop करने के बजाय cross-fade करते हैं। दो unit vectors का normal blend खुद unit length का नहीं होता, इसलिए इसे फिर से normalize किया जाता है, जो पड़ोसी tiles के बीच के छोटे कोणों के लिए एक slerp जैसा बर्ताव करता है। कीमत असली है (एक 12×12 atlas az/el के 1.6 MB के मुकाबले करीब 9 MB का है, और bake 288 render-target passes पर लगभग 5× ज़्यादा लंबा चलता है) पर bake load के वक्त एक बार का होता है और blend एक pop-free नतीजा देता है, जो imposters को तब इस्तेमाल लायक बनाता है जब camera वाकई चल रहा हो।

दस लाख पेड़, प्रति frame एक camera-position copy

Spike 41 को नए tab में खोलें ↗ · Source देखें

Spike 38 का runtime प्रति quad प्रति frame एक CPU lookAt करता है, जो एक पेड़ के लिए ठीक है और एक जंगल के लिए जानलेवा। दस लाख पेड़ों पर प्रति-frame matrix updates और instance-buffer upload सब कुछ हावी कर देंगे। Spike 41 पूरी per-frame pipeline को GPU पर ले जाता है। हर instance का center, yaw, और scale build time पर एक बार instanced attributes के रूप में upload होता है और कभी नहीं बदलता। Vertex shader world-space view direction camPos − center से billboard basis बनाता है और एक साझा unit quad को world space में फैलाता है। Fragment shader प्रति pixel hemi-octahedral encode और bilinear blend करता है। पूरे जंगल के लिए एकमात्र per-frame CPU काम है camera-position uniform को update करने वाला एक Vector3.copy, जो पेड़ों की गिनती के साथ बिल्कुल भी नहीं बढ़ता।

गणित का एक अच्छा हिस्सा यह है कि per-instance yaw normal decode से कट जाता है। Bake normals को bake camera के frame में store करता है, और चूँकि world up के चारों ओर एक yaw rotation +Y को बरकरार रखता है और cross product rotation-equivariant है, इसलिए world-up reference के साथ बना runtime basis पहले से ही rotated bake basis के बराबर है। तो shader normals को runtime basis varyings के ज़रिए सीधे decode करता है, per-instance yaw को कभी छुए बिना। Placement शुद्ध random scatter के बजाय एक jittered grid इस्तेमाल करता है: इलाके को cells में बाँटो, हर cell में center पर एक पेड़ डालो और साथ में एक bounded offset, जो एक न्यूनतम spacing की गारंटी देता है (कोई दो पेड़ एक दूसरे के ऊपर नहीं) जबकि फिर भी एक प्राकृतिक जंगल जैसा पढ़ा जाता है। एक detail जिसे चूकना आसान है वह है bounding sphere। Geometry template बस एक unit quad है, इसलिए three.js पूरे जंगल को उसी पल frustum-cull कर देता जिस पल camera origin से दूर देखता। पूरे इलाके को और एक quad के margin को ढकने वाली एक explicit bounding sphere set करना कोने के पेड़ों को तिरछे कोणों पर कटने से बचाता है।

इस अध्याय में उल्लिखित technology

Octahedral और azimuth-elevation imposter atlases. एक imposter किसी prop को view directions के एक grid से एक diffuse atlas और एक world-space normal atlas में bake करता है, फिर एक ही billboard render करता है जो मेल खाती tile sample करता है, हज़ारों triangles को दो textures से बदल देता है। किताबी octahedral mapping uniform sphere coverage देती है पर fold boundaries पर CPU/GPU divergence की ओर झुकी रहती है; एक सादा azimuth-बनाम-elevation grid एक कम uniform sphere sample करता है पर construction से ही गारंटी देता है कि cell-picker और shader सहमत रहें। Odd N इस्तेमाल करें ताकि एक row ठीक 0° elevation पर आए, और tile budget को horizontal ring पर खर्च करें क्योंकि props ज़्यादातर आँख की ऊँचाई से देखे जाते हैं।

Piecewise-static billboard orientation. एक imposter एक खास bake direction से ली गई तस्वीर है, इसलिए runtime quad के image plane को bake camera के plane से मेल खाना ज़रूरी है, runtime camera की ओर लगातार मुँह करना नहीं। Quad उस arc भर में अपना orientation थामे रखता है जहाँ एक cell चुनी रहती है, फिर boundary पर snap हो जाता है, जो imposter दूरी पर अदृश्य रहता है और सिर्फ करीब आने पर pop करता है जहाँ imposters इस्तेमाल नहीं होते।

Hemi-octahedral atlas with bilinear cell blend. ऊपरी hemisphere को unit square पर लगातार map करने से pole singularity और खास top-down tile हट जाती है। Encode की गई view direction को घेरने वाले 2×2 tile group को sample करके और चारों tiles को bilinear-blend करके (8 taps) snap को खत्म किया जाता है, इसलिए पास-पास के views cross-fade करते हैं। Blended normals को फिर से normalize किया जाता है, जो छोटे inter-tile कोण पर एक slerp के करीब आता है। कीमत है एक बड़ा atlas (12×12 पर करीब 9 MB) और एक लंबा एक बार का bake, जो camera की गति में pop-free shading के बदले में दिया जाता है।

GPU-driven instanced imposters. Per-instance center, yaw, और scale एक बार instanced attributes के रूप में upload होते हैं; vertex shader billboard basis बनाता है और एक साझा unit quad को फैलाता है, और fragment shader प्रति pixel encode और blend करता है। पूरे जंगल के लिए per-frame CPU कीमत एक अकेली camera-position uniform copy है, जो instance count से स्वतंत्र है। Per-instance yaw normal decode से कट जाता है क्योंकि world up के चारों ओर yaw cross-product basis construction से बरकरार रहता है। एक explicit जंगल-भर की bounding sphere three.js को पूरे instanced mesh को frustum-cull करने से रोकती है जब camera unit-quad template के origin से दूर देखता है। देखें GPU-driven LOD


29 में से भाग 19। पिछला: भाग 18 - एक scatter brush जो AI-placed महसूस होता है अगला: भाग 20 - एक चपटे plane पर depth का भ्रम Series guide: /hi/blog/2026-02-25-open-world-browser-series-guide