Skip to content

Browser में open world बनाना, भाग 26: पानी बनाने के तीन तरीके

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

यहां नए हैं? सीरीज़ गाइड इस्तेमाल करें। यह बताता है कि spike क्या है और सारे भागों को link करता है।

भाग 25 ने avatars को कपड़े पहनाए। यह भाग पानी के बारे में है, और इसमें तीन spike हैं क्योंकि पानी वही surface है जहां एक सस्ता shortcut और सही जवाब screenshot में बिलकुल एक जैसे दिखते हैं और motion में पूरी तरह अलग। Spike 51 reflection को screen-space तरीके से बनाता है, वो लुभावना तरीका, और सीधे अपनी built-in सीमा से टकरा जाता है। Spike 52 उस method पर switch करता है जिसे हर शिप होने वाला game असल में इस्तेमाल करता है। Spike 53 एक तैयार water library डालकर देखता है कि "done" हमसे कितनी दूर है। तीनों एक ही refraction layer share करते हैं, इसलिए पहले दो के बीच सिर्फ एक चीज़ बदलती है: reflection कैसे compute होता है।

उसी screen से reflection जो आपके पास पहले से है

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

Screen-space reflection उसी frame को दोबारा इस्तेमाल करता है जिसे आप पहले ही render कर चुके हैं। हर water pixel के लिए, आप view ray को surface से reflect करते हैं, उस reflected ray को depth buffer के आर-पार march कराते हैं, और जब ray किसी record की गई surface के पीछे से गुज़रता है तो आपको मिल गया कि पानी क्या reflect करता है, सीधे color buffer से sample किया हुआ। यह port three.js की SSRNode line को line-by-line follow करता है, जो खुद lettier के SSR primer को follow करती है। March screen space में एक DDA walk है: ray के start और end को pixel coordinates में project करें और जो भी axis लंबा हो उसके साथ step करें, हर pixel पर एक tap। हर step पर reflected ray की depth को perspective-correct interpolation चाहिए, 11/z0+s(1/z11/z0), क्योंकि view-Z को linearly interpolate करना सीधे-सीधे गलत है और गलत जगह पर hits पैदा करता है।

दो चीज़ें इसे slideshow की जगह इस्तेमाल करने लायक बनाती हैं। Coarse march 64 steps पर capped है, क्योंकि एक लंबा ray जो हज़ार pixels के पार project होता है वरना हर fragment के लिए सैकड़ों iterations चला देता, और दस लाख-fragment वाला water plane गुणा सैकड़ों iterations गुणा कुछ texture samples एक 30 fps scene है। Quality उस cap के अंदर effective stride को control करती है, iteration count को नहीं। और चूंकि एक capped coarse march दिखने वाली stair-step banding छोड़ता है, इसलिए एक छह-iteration वाला binary refinement आखिरी miss और hit के बीच के interval को bisect करता है, जो 64x sub-step accuracy है, इतनी कि पड़ोसी water fragments उसी coarse hit position पर lock होना बंद कर देते हैं। एक आखिरी point-to-line distance check पुष्टि करता है कि candidate सचमुच reflection ray पर है, न कि सिर्फ उसी depth पर, एक thickness tolerance के साथ जो उस depth पर एक pixel की view-space width के हिसाब से auto-scale होती है, पास में tighter और दूर में looser।

इस spike का ईमानदार हिस्सा उसके अपने comments में लिखा है: SSR उस चीज़ को reflect नहीं कर सकता जिसे main camera ने कभी sample नहीं किया। किसी पेड़ का नीचे का हिस्सा, off-screen कुछ भी, occluded कुछ भी, इनमें से कुछ भी buffers में मौजूद नहीं है, इसलिए reflection में दिख ही नहीं सकता। यही वो "wrong-side information loss" है जिसे कितनी भी march quality ठीक नहीं करती, और यही ठीक वो वजह है जिसके लिए अगला spike मौजूद है।

वो mirror जो झूठ नहीं बोल सकता

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

एक planar reflection scene को दूसरी बार render करता है, water plane के आर-पार mirror की गई एक camera से, एक offscreen target में, और water shader उस target को sample करता है। यह canonical pattern है, जिसे UE5 Water, three.js की अपनी WaterMesh, ABZÛ, और Sea of Thieves सब इस्तेमाल करते हैं, क्योंकि इसके पास draw करने के लिए scene का हर pixel मौजूद है, उस geometry समेत जिसे SSR कभी देख नहीं सकता। TSL में यह लगभग anticlimactic है: reflector() mirror की गई helper camera और उसका render target allocate करता है, आप उसके target को mesh में जोड़ते हैं ताकि वह हर frame update हो, और आप उसका color sample करते हैं। जब बाद में waves आती हैं, तो reflection reflector के UV node में एक distortion offset जोड़कर wobble करता है, जो ठीक वही line है जो WaterMesh इस्तेमाल करती है।

record करने लायक bug safety net में था, mirror में नहीं। एक पुराने version ने reflector के output को एक procedural sky के साथ fallback के तौर पर blend किया, reflected color की magnitude के हिसाब से weighted, इस theory पर कि करीब-शून्य reflection का मतलब है target में वहां कुछ नहीं था। पर एक dark canopy की छाया की magnitude भी कम होती है, इसलिए clamp कभी full strength तक नहीं पहुंचा और वो असल में dark pixels bright sky के साथ mix हो गए। user ने जो symptom पकड़ा वह सटीक था: debug mode में raw mirror target perfect dark पेड़ दिखा रहा था, जबकि composed render में washed-out reflections थे, और diagnosis यह था कि shader "dark colors को गायब कर देता है।" Fix था deletion। Reflector target पहले frame के बाद reliable रहता है, इसलिए fallback की कोई ज़रूरत ही नहीं। दोनों spike नीचे वही refraction share करते हैं: surface के पीछे का scene sample करें, reconstruct करें कि हर pixel waterline से कितना नीचे बैठा है, और per-channel Beer-Lambert extinction लगाएं ताकि red कुछ ही meters में मर जाए जबकि blue बना रहे, एक sky mask के साथ ताकि far-plane backdrop fog न हो जाए। Schlick Fresnel refraction को तब mix करता है जब आप सीधे पानी में नीचे देखते हैं और reflection को तब जब आप उसके पार देखते हैं।

पूरा हो चुका कैसा दिखता है

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

Spike 53 build-versus-buy check है। यह threejs-water-pro को वैसा ही डालता है जैसा वह ship होता है, अपने tropical preset, default ocean clipmap, camera tracking, और Rayleigh sky के साथ, और वही island glTF load करता है जो library का अपना demo इस्तेमाल करता है। मकसद है एक hand-rolled flat-water shader और एक पूरे ocean system के बीच का फ़र्क़ देखना, और यह फ़र्क़ बड़ा है: इस वाले में एक buoyancy system है जो किसी ship के hull के नीचे कई points पर wave height sample करता है ताकि वह सिर्फ ऊपर-नीचे होने की जगह pitch और roll करे, एक wake generator, shoreline और surface foam, और एक mask pass जो ship के hull के अंदर water rendering को दबा देता है ताकि ripples deck के आर-पार न रिसें।

इसे integrate करने से वो किस्म का detail सामने आया जो आप किसी library को इस्तेमाल करके ही सीखते हैं, उसका README पढ़कर नहीं। Foam textures को preset में filename से reference किया जाता है पर उन्हें load करना consumer का काम है, और उनके बिना shoreline breaking surf की जगह एक सख्त waterline edge जैसा दिखता है। Island ऐसे positioned है कि उसकी underwater geometry ocean floor के पार नीचे गिर जाती है, जिससे library का floor mesh model के बाहरी ring को बिना किसी दिखने वाले plane edge के occlude कर देता है, जो library का intended pattern है: एक 3D model लाओ, terrain synthesize मत करो। और antialiasing renderer पर जानबूझकर off है, एक post-process FXAA pass के पक्ष में, क्योंकि MSAA depth-aware atmospheric fog चलने से पहले edge fragments को background के against blend कर देता है, जहां भी geometry fog से मिलती है वहां एक पतला dark fringe छोड़ देता है। Aliasing को fog से पहले की जगह fog के बाद resolve करने से वह fringe हट जाता है। यह जो de-risk करता है वह खुद decision है: एक production ocean एक बड़ा, specialized system है, और जिन cases में हमें एक चाहिए, वहां एक maintained library अपनाना wakes और buoyancy और foam को शुरू से दोबारा बनाने से बेहतर है, जबकि spike 52 का planar-mirror shader उस छोटे inland water के लिए सही जवाब बना रहता है जिसे कोई creator अपनी दुनिया में रखता है।

इस chapter में reference की गई technology

Screen-space reflection water. एक reflected view ray DDA के ज़रिए screen space में depth buffer को march करता है, perspective-correct 1/z interpolation का इस्तेमाल करते हुए, per-fragment cost को bound करने के लिए एक hard step cap, और एक capped march जो banding छोड़ता है उसे हटाने के लिए एक binary refinement pass। depth-scaled thickness वाला एक point-to-line confirmation false hits को reject करता है। Method की hard limit यह है कि यह सिर्फ वही geometry reflect कर सकता है जिसे main camera पहले ही sample कर चुका है, इसलिए off-screen और wrong-side surfaces कभी नहीं दिखतीं। देखें terrain materials

Planar mirror reflection. Water plane के आर-पार mirror की गई एक helper camera scene को एक offscreen target में render करती है जिसे water shader sample करता है, जो pixel-perfect reflections देता है, उस geometry समेत जिसे SSR देख नहीं सकता। यह वही pattern है जो UE5 Water और three.js WaterMesh इस्तेमाल करते हैं, wave distortion को reflector के UV node पर एक offset के तौर पर लगाया जाता है। एक magnitude-weighted sky fallback ने गलती से dark reflection pixels मिटा दिए; reflector target पहले frame के बाद reliable रहता है, इसलिए fallback हटाना ही fix था।

Beer-Lambert depth tint refraction. दोनों shaders surface के पीछे का scene sample करते हैं, हर backdrop pixel की waterline से नीचे की depth reconstruct करते हैं, और per-channel exponential extinction (red meters में मरता है, blue बना रहता है) लगाते हैं जो एक water-fog color की तरफ composite होता है, एक sky mask के साथ ताकि far plane fog न हो। Schlick Fresnel normal incidence पर refraction को grazing angles पर reflection के साथ blend करता है।

एक production water library अपनाना. threejs-water-pro एक ocean clipmap, Rayleigh sky, ship के pitch और roll के लिए multi-point buoyancy, wakes, foam, और एक hull mask pass ship करती है। Consumer-side के details मायने रखते हैं: foam textures को explicitly load करना ज़रूरी है, island की underwater geometry को ocean floor के पार नीचे गिरना चाहिए ताकि floor उसके edges को occlude करे, और antialiasing को MSAA की जगह एक post-process FXAA pass के तौर पर चलना चाहिए ताकि geometry edges पर एक dark fog fringe से बचा जा सके।


29 में से भाग 26। पिछला: भाग 25 - एक skeleton, हर outfit अगला: भाग 27 - noise से एक island, ज़मीन जो ज़मीन जैसी दिखे सीरीज़ गाइड: /blog/2026-02-25-open-world-browser-series-guide