فهرست مستندات

تکنیک‌های سه‌بعدی zoneهای پارک#

پارک (environments/park) محیط first-party اجتماعی-طبیعی الامر است: یک قلب باغ که هشت دروازهٔ محلی دور آن حلقه زده‌اند و هر کدام به یک Zone می‌رسند. سه zone منتشر شده است. شکوفه‌زار مرجانی (coral-bloom) باغی در نور طلایی غروب از درختان شکوفهٔ مرجانی است که گرد یک برکهٔ آرام جمع شده‌اند، با لانه‌های گلبرگ روی چمن‌زار و گلبرگ‌هایی که با نسیم پرواز می‌کنند. ایوان ابر (cloud-terrace) جنگلی بهشت‌گونه در دو تراز است: یک فلات بالایی که ۱۴ متر بالاتر از درهٔ پایینی قرار دارد، رودخانه‌ای پهن و پیچ‌خم‌دار که از گسست تراز به‌صورت آبشار فرو می‌ریزد، و حوضچه‌ای در پای آبشار. بیشهٔ مهتاب (moon-grove) بیشه‌ای از غان‌های نقره‌پوش زیر آسمان نیلی صاف شب است — پوست نقره‌ای غان‌ها، باند بیرونی از درختان خشکیده که مه آن را می‌پوشاند، و صفایی باز در قلبش. این نخستین zone مدل‌محور پروژه هم هست: هندسهٔ پوشش گیاهی‌اش به‌جای سازنده‌های رویه‌ای از پک‌های .glb با مجوز CC0 از Quaternius و از طریق لودر مشترک useGltfPack می‌آید.

هر سه zone روی یک پشتهٔ صحنهٔ مشترک سوارند — آسمان، مالک post-processing، رقص بوت، و نظم مدل‌های خالص — و تمام تمایزشان از طریق داده است: یک ParkZoneDefinition مخصوص هر zone در environments/park/src/zones/registry.ts. این سند همهٔ تکنیک‌هایی را که این zoneها را کار می‌کنند مرور می‌کند و برای هر ادعا ماژول منبع را می‌آورد تا با کد قابل راستی‌آزمایی باشد. نمایهٔ رندرینگی که این پشته روی آن بنا شده در زمان اجرای رندرینگ و ADR-0048 توصیف شده است.

معماری رندرینگ و مالکیت بوم#

مرز گرافیکی ثابت و لایه‌ای است (EnvironmentRenderingGate -> AlAmrProvider -> AlAmrRuntimeGate -> EnvironmentCanvas) و دقیقاً یک بوم مالک renderer است. state مربوط به spawn، pose، مسیر و activeZoneId همگی بیرون از بوم و در ParkExperience.tsx زندگی می‌کنند، پس بازیابی renderer (گم‌شدن device، fallback از WebGPU به WebGL2) صحنه را remount می‌کند بدون آنکه بازدید از نو شروع شود.

renderer همان WebGPURenderer از three/webgpu است که WebGPU را ترجیح می‌دهد و در نبودش روی همان renderer به backend داخلی و واقعی WebGL2 برمی‌گردد. تمام مواد سفارشی در هر دو zone گراف TSL از جنس NodeMaterial هستند — هیچ GLSL دست‌نویسی در کار نیست — پس همان گراف‌ها بسته به backend به WGSL یا GLSL کامپایل می‌شوند. ترکیب صحنه با React Three Fiber است و کیفیت از طریق useRenderingQuality() از @al-amr/r3f resolve می‌شود.

صحنه‌های zone رجیستری‌محور و code-split هستند. هر ورودی PARK_ZONES یک loadScene() با import پویا، یک پروفایل هنری (resolveSky به‌علاوهٔ overrideهای اختیاری post)، یک مدل ناوبری (sampleGroundHeight، walkableRadiusM، flightCeilingM، resolveObstacles)، جای spawn و پورتال بازگشت، بودجه‌های رندر و مکان‌های معنایی متعلق به zone را اعلام می‌کند. zone-scene-loader.ts صحنهٔ هر zone را دقیقاً یک بار per zone id در React.lazy می‌پیچد — یک lazy identity پایدار، چون ساختن دوبارهٔ wrapper صحنه را remount می‌کند و پیشرفت بوت را از بین می‌برد — و wrapper کش‌شده را فقط در مسیر retry حذف می‌کند، چون React.lazy یک import ناموفق را برای همیشه به خاطر نگه می‌دارد. سفر hub-and-spoke است: zoneهای دروازه فقط از طریق returnPortal خود به garden-heart برمی‌گردند.

یک قاعدهٔ ساختاری آگاهانه: ParkPostProcessing تنها مالک رندر است. یک بار و در سطح ParkExperience داخل بوم mount می‌شود، هرگز داخل صحنهٔ یک zone نه، پس تعویض zone می‌تواند کل ترکیب را جایگزین کند بدون آنکه pipeline remount شود (این قاعده با یک تست معماری پین شده است).

سه سطح کیفیت#

هر zone هزینهٔ واقعی را بین سه سطح enhanced / standard / compatible مقیاس می‌دهد — هرگز فقط رزولوشن را نه. سطح compatible کار fragment، کار instance و passها را سراسر حذف می‌کند:

جنبهenhancedstandardcompatible
اکتاوهای FBM ابرهای آسمان653 بدون لایهٔ جزئیات billow
مسیر postMRT pipeline + bloom + FXAAMRT pipeline + bloom + FXAAرندر مستقیم، بدون MRT/bloom/FXAA
نقشهٔ سایه204810241024 بدون castShadow روی پوشش گیاهی
درختان / چمن / گل‌های شکوفه‌زار مرجانی760 / 22,000 / 20,000520 / 15,000 / 14,000210 / 6,500 / 6,000
درختان / چمن ایوان ابر900 / 26,000620 / 18,000240 / 8,000
غان‌ها / چمن بیشهٔ مهتاب170 / 3,000110 / 1,80036 / 520
نویز زمینلکه‌های mx_noise_floatلکه‌های mx_noise_floatباندهای خالص سینوسی
مقیاس حرکت محیطی110.35

تعداد draw call بین سطح‌ها هرگز تغییر نمی‌کند؛ فقط تعداد instanceها و هزینهٔ گراف عوض می‌شود. ساعت‌های محیطی سرعتشان را از resolveParkMotionScale(quality, reducedMotion) در environments/park/src/park-model.ts می‌گیرند — ترجیح reduced motion همهٔ ساعت‌ها را به ۰ فریز می‌کند و compatible آن‌ها را با ۰٫۳۵ اجرا می‌کند — و useReducedMotionPreference() در environments/park/src/scenes/motion-preference.ts تنها خوانندهٔ reactive آن media query است، پس همهٔ ساعت‌ها با هم توافق دارند و با هم می‌ایستند.

آسمان: گنبد درون‌صحنه، نه post-processing#

آسمان هندسه است، نه افکت صفحه. ParkSkyDome (environments/park/src/scenes/sky/ParkSky.tsx) یک مش SphereGeometry(500, 48, 24) با BackSide و یک MeshBasicNodeMaterial است که کل ظاهرش از normalize(positionLocal) محاسبه می‌شود — یک میدان جهت، پس شعاع کره صرفاً ظاهری است. گراف fragment آن لایه‌لایه است:

  • گرادیان جهت‌محور (از رنگ سماوی به افق، با گرم‌شدن افق در سمت خورشید با pow(saturate(cosSun), 3)
  • ابرهای FBM که روی صفحه‌ای بالای سر پروجکت می‌شوند (direction.xz / max(direction.y, 0.06)) و با time داخلی TSL رانش دارند،
  • قرص خورشید HDR و هالهٔ طلایی تنگ آن که از emissiveNode عبور می‌کنند تا پاس انتخابی bloom آن‌ها را بگیرد، در حالی که درخشش پهن فقط در پاس زیبایی می‌ماند.

سه ترفند گنبد را نگه می‌دارند. اول، گره vertex هر fragment را به صفحهٔ دور پین می‌کند و گنبد را از فاصلهٔ far دوربین مستقل می‌سازد:

material.vertexNode = Fn(() => {
  const clip = modelViewProjection as unknown as ClipSpaceNode;
  clip.z.assign(clip.w); // pin the dome to the far plane
  return clip;
})() as unknown as NodeMaterial["vertexNode"];

دوم، گنبد هر فریم دوربین را دنبال می‌کند (mesh.position.copy( camera.position) در useFrame)، با renderOrder = -1000 و depthWrite: false رندر می‌شود و frustumCulled = false دارد، پس همیشه پشت همهٔ مش‌های جهان می‌نشیند. سوم، حلقهٔ اکتاوهای ابر در زمان ساخت گراف و در جاوااسکریپت خالص unroll می‌شود — تعداد اکتاوها یک مقدار JS است، پس compatible واقعاً گراف fragment ارزان‌تری می‌سازد (فراخوانی‌های mx_noise_float کمتر، بدون لایهٔ billow) به جای آنکه فقط DPR را پایین بیاورد. و چون بازهٔ FBM با تعداد اکتاوها رشد می‌کند، تجمع بر parkCloudAmplitudeSum(octaves) تقسیم و دوباره روی ۰٫۵ مرکز می‌شود تا smoothstep پوشش ابر، همان میدان نرمال‌شده را در هر سطح برش بزند: پوشش ابر در همهٔ کیفیت‌ها یکسان خوانده می‌شود.

zoneها هرگز این پشته را بازسازی نمی‌کنند. *-sky-model.ts هر zone (resolveCoralBloomSky، resolveCloudTerraceSky، resolveMoonGroveSky) همان ساختار ParkSkySpec را با پالت، مه، ویژگی‌های خورشید و بودجهٔ ابر خودش برمی‌گرداند — شکوفه‌زار مرجانی آبی تازه و اشباع که سقف مرجانی را جلوه می‌دهد، ایوان ابر آسمان روشن و high-key بهشتی (skyBrightness برابر ۱٫۵۵، پوشش ابر ۶۲٪، مه تقریباً سفید)، و بیشهٔ مهتاب شبی صاف و نیلی (skyBrightness برابر ۰٫۶۲، پوشش ابر ۱۶٪) که قرص HDR آن از خورشید به ماه نقره‌ای بازرنگ‌بندی شده است. خود جهت خورشید مشترک است (PARK_SUN_DIRECTION، خورشید گرمای اواخر ظهر با ارتفاع حدود ۲۵ درجه)، پس جهان‌ها دو سوی پردهٔ دروازه سازگار می‌مانند — در بیشهٔ مهتاب همان بردار، ماه است و قرص آسمان، نور سایه‌دار و هر برقی همچنان با هم توافق دارند. Fog خطی صحنه دقیقاً از hazeColor پالت استفاده می‌کند — مه همان هالهٔ زیرافق گنبد است — پس لبهٔ زمین هرگز با آسمان درز نمی‌کند. بیشهٔ مهتاب بیش از همه روی همین تکیه می‌کند: مه آن (۹۰ تا ۲۸۰ متر) جزئی حیاتی از هنر زون است، باند بیرونی را می‌پوشاند و به سطح compatible اجازه می‌دهد جمعیت درختان را کم کند بدون آنکه لبهٔ جهان دیده شود.

نور و سایه#

یک directionalLight سایه‌دار (هم‌راستا با PARK_SUN_DIRECTION) به‌علاوهٔ یک hemisphereLight به‌عنوان نور پرکننده، کل zone را روشن می‌کنند؛ پالت، شدت‌ها و frustum سایه همگی از ParkSkySpec همان zone می‌آیند. بسط سایهٔ ارتوگرافیک per zone تالیف شده (CORAL_BLOOM_SHADOW_EXTENT_M برابر ۹۲، CLOUD_TERRACE_SHADOW_EXTENT_M برابر ۱۰۴ و MOON_GROVE_SHADOW_EXTENT_M برابر ۸۸) با یک ثابت guardrail زمان کامپایل که تضمین می‌کند frustum همیشه کل دیسک zone را پوشش می‌دهد. اندازهٔ نقشهٔ سایه تنها دستگیرهٔ سطح است (2048 در enhanced، وگرنه 1024) و مش‌های پوشش گیاهی فقط خارج از سطح compatible سایه می‌اندازند (castShadow) — resolveParkRenderPolicy در آنجا سایه‌های صحنه را حذف می‌کند به جای آنکه رزولوشن سایه را پایین بیاورد.

پس‌پردازش#

ParkPostProcessing (environments/park/src/scenes/post/ParkPostProcessing.tsx) مالک فریم است. اشتراک useFrame آن با اولویت ۱، رندر خودکار R3F را غیرفعال می‌کند، پس این کامپوننت در هر مسیر هر فریم را رندر می‌کند. در standard و enhanced یک RenderPipeline از three اجرا می‌کند: صحنه یک بار در یک هدف MRT با attachmentهای output و emissive رندر می‌شود، bloom انتخابی فقط از attachment امیسیو محاسبه و روی پاس زیبایی افزوده می‌شود، سپس یک renderOutput صریح با ACES filmic و sRGB تصویر ترکیبی را tone-map می‌کند و FXAA در انتها می‌آید — تبدیل خروجی داخلی pipeline خاموش است چون FXAA به ورودی display-referred نیاز دارد:

const scenePass = pass(scene, camera);
scenePass.setMRT(mrt({ output, emissive }));
const bloomNode = bloom(scenePass.getTextureNode("emissive"), strength, radius, threshold);
pipeline.outputColorTransform = false; // FXAA needs display-referred input
pipeline.outputNode = fxaa(
  renderOutput(
    scenePass.getTextureNode("output").add(bloomNode),
    ACESFilmicToneMapping,
    SRGBColorSpace,
  ),
);

مسیر compatible کل pipeline را کنار می‌گذارد: یک gl.render(scene, camera) مستقیم با tone mapping امیسیوی ACES per material — هزینهٔ واقعی حذف می‌شود، نه نسخهٔ ارزان‌تری از همان زنجیره. tone mapping دقیقاً یک بار per مسیر اعمال می‌شود (روی renderer در مسیر pipeline NoToneMapping است چون تبدیل خروجی صریح pipeline مالک آن است) و کلید بازسازی pipeline به پیکسل‌های واقعی drawing buffer گرد می‌شود تا تغییر اندازه‌های زیرپیکسلی CSS هدف‌های رندر را تلاطم ندهند.

جهت هنری zone به‌صورت prop می‌رسد و با جهش uniform اعمال می‌شود، هرگز با بازسازی گراف نه: bloomNode.strength.value و رفقایش هر فریم به‌روز می‌شوند و exposure روی renderer.toneMappingExposure نوشته می‌شود که یک uniform زندهٔ rendererReference است و هر دو مسیر هر فریم آن را می‌خوانند. شکوفه‌زار مرجانی قدرت bloom را به ۰٫۶۲ می‌رساند؛ ایوان ابر bloom را ۰٫۷ با exposure ۱٫۱ نسبت به پیش‌فرض تالیف‌شدهٔ ۱٫۰۴ اجرا می‌کند؛ و بیشهٔ مهتاب با ۰٫۵۵ ملایم‌تر می‌ماند تا فقط لبه‌های مهتابی، گل‌ها و پردهٔ بازگشت بدرخشند — نه کل صحنهٔ شب. حس درخشان بهشت از همین راه ساخته می‌شود — قرص خورشید HDR، برق‌های خورشید روی آب و جرقه‌های آبشار همگی attachment امیسیوی MRT را تغذیه می‌کنند و bloom دقیقاً همان‌ها را می‌گیرد.

زمین#

زمین هر zone یک دیسک قطبی جابه‌جاشده است (createCoralTerrainGeometry، createCloudTerraceTerrainGeometry) که از تابع ارتفاع خالص خودِ zone نمونه می‌گیرد و per سطح tessellate می‌شود (شکوفه‌زار مرجانی از ۲۰۰×۴۰ سگمنت شعاعی/حلقه‌ای در enhanced تا ۸۸×۱۸؛ ایوان ابر از ۲۲۴×۴۶ تا ۹۶×۲۰). مادهٔ آن گراف TSL چمن‌زار لایه‌ای بدون بافت است: سبزهای عمیق/چمن‌زاری/آفتابی که با دو مقیاس نویز لکه ترکیب می‌شوند (mx_noise_float در سطح‌های detailed، که در compatible با باندهای خالص سینوسی جایگزین می‌شود — هزینهٔ واقعی fragment حذف می‌شود)، یک حاصل‌ضرب سینوسی برای بافت ریز تیغه‌ها در نمای نزدیک، قله‌های آفتاب‌خورده بر اساس ارتفاع، و حلقهٔ تیرهٔ مرطوب نزدیک آب، با roughnessNode که با همان بافت ریز مدوله می‌شود.

شکوفه‌زار مرجانی یک باند صورتی گلبرگ دور قلب باغ اضافه می‌کند — رنگ‌گرفتگی چمن از ریزش شکوفه بین حوضچهٔ برکه و میدان میانی. ایوان ابر دو ترفند امضایی اضافه می‌کند: باند مرطوب کنار رود که از یک دوقلوی TSLِ خط مرکزی پیچ‌خم‌دار محاسبه می‌شود (sin(z*0.016+0.35)*11 + sin(z*0.037-0.8)*5، ضرایب از cloudTerraceRiverCenterX کپی شده و علامت‌گذاری شده که باید همگام بمانند) و لحن صخری خاکستری-کرم صخره‌ای جایی که normalLocal.y شیب تند گزارش می‌کند، که به باند گسست تراز و ارتفاع‌های دیوارهٔ صخره محدود شده تا چمن‌زارهای فلات هرگز به سنگ تبدیل نشوند. ترفند سوم، نور متحرک caustic روی بستر غرق‌شدهٔ رودخانه و کف حوضچهٔ سقوط است: تداخل دو مجموعهٔ سینوسی جهت‌دار به‌علاوهٔ یک بافت عبوری، تیزشده با یک pow و ماسک‌شده با فاصله از رود، حوضچهٔ سقوط و یک پنجرهٔ عمق در برابر دوقلوی TSLِ cloudTerraceRiverWaterY، پس الگو به آبِ بالای سر نیاز دارد و در قلب عمیق حوضچه محو می‌شود. مثل آب‌سفید پرده، خالص سینوسی است (بدون mx_noise)، پس در هر سطح می‌ماند و فقط با مقیاس حرکت پارک کند می‌شود.

آب#

هر دو zone روی هر backend آب TSL قابل‌حمل اجرا می‌کنند — هیچ مسیر reflector بلادرنگی در کار نیست — که بودجهٔ draw call را حفظ می‌کند و برابری WebGL2 را دقیق نگه می‌دارد. تمام انیمیشن آب داخل گراف‌های node زندگی می‌کند؛ useFrame فقط یک uniform زمان مشترک per ماده را جلو می‌برد که با قاعدهٔ حرکت پارک مقیاس می‌شود.

برکهٔ شکوفه‌زار مرجانی (CoralBloomPond.tsx) یک دیسک تنها در CORAL_BLOOM_POND_WATER_Y است. سه موج سینوسی جهت‌دار لایه‌ای یک میدان نرمال تحلیلی می‌سازند (شیب‌های کسینوسی، بدون تفاضل محدود) به‌علاوهٔ موجی ملایم فیزیکی در positionNode که مجموعاً ۰٫۰۴۱ متر است — به‌خوبی درون حاشیهٔ آزاد حوضچه. رنگ بدنه با عمق درجه‌بندی می‌شود و به کم‌عمقی‌های غروب گرم می‌رسد، فرنل با توان ۵ رنگ آسمان را بلند می‌کند، یک برق خورشید HDR (pow(sunAmount, 340)، تقویت‌شده با جملهٔ twinkle) و یک خال‌خالی caustic به emissiveNode می‌رسند تا پاس bloom آن‌ها را بگیرد، و تور پوششی کف در لبهٔ حوضچه نفس می‌کشد.

رودخانهٔ ایوان ابر (CloudTerraceRiver.tsx) چهار مش روی یک ساعت است: دو روبان تخت که خط مرکزی پیچ‌خم‌دار را دنبال می‌کنند (فلات بالا و درهٔ پایین)، یک پردهٔ عمودی آبشار در گسست تراز، و حوضچهٔ سقوط. سازندهٔ روبان uv.v را بر حسب مترِ جریان می‌نویسد، پس هر دو قطعه با سرعت جهانی یکسان از یک گراف اسکرول می‌شوند:

// uv.v is authored in flow-metres by the ribbon builder, so both stretches scroll at one world speed
const p = vec2(uv().x.mul(riverWidth), uv().y);
const phaseA = dot(p, dirA).mul(0.55).sub(time.mul(1.7)); // dirA = (0, 1): straight downstream

پردهٔ آبشار مختصاتش را با شتاب درجهٔ دو (v + v*v*0.9) تاب می‌دهد تا باندهای کشش به سمت حوضچه فشرده شوند — آب در حال سقوط شتاب می‌گیرد — رگه‌های جویبار، نویز آب‌سفید حاصل‌ضرب دو باند سینوسی (عمداً بدون mx_noise تا پرده در هر سطح هزینهٔ یکسان داشته باشد)، انفجار روشن نقطهٔ فرود و جرقهٔ HDR در emissiveNode را اضافه می‌کند؛ کدورت در هر دو کرانه و لبهٔ آبشار پرده‌ای محو می‌شود. حوضچهٔ سقوط همان دستور برکه را بازاستفاده می‌کند و حلقهٔ کف درحال‌انبساطی که از نقطهٔ برخورد تالیف‌شدهٔ پرده پخش می‌شود، لکه‌های کف شناور و جرقهٔ امیسیو روشنِ مه‌مانند درون حلقه را اضافه می‌کند.

دو سیستم instanced کوچک از CloudTerraceRiverEffects.tsx روی آب سوارند (یک draw هر کدام در هر سطح — کیفیت فقط تعداد نمونه‌ها را کم می‌کند). خرده‌های شناور — خرده‌های کف و برگ‌های ریز افتاده — روی سطح رودخانهٔ بالایی حرکت می‌کنند: گراف vertex هر خرده را در یک حلقهٔ جریان روی دوقلوی TSLِ خط مرکزی جلو می‌برد، آن را روی دوقلوی تراز آب بالادست به‌علاوهٔ موج سه‌موجهٔ خودِ رودخانه می‌نشاند و صفحهٔ تخت را آرام می‌چرخاند؛ fragment بیضی خرده و پوش حلقه را برش می‌زند، پس خرده‌ها در بالادست ظاهر و از لبهٔ آبشار سرازیر می‌شوند و دوباره به بالادست برمی‌گردند. مه برخورد آبشار، مشتی sprite نرم روبه‌دوربین در نقطهٔ برخورد حوضچه است — مشتق از ریاضی رودخانهٔ مدل (CLOUD_TERRACE_WATERFALL_IMPACT_*)، هرگز هاردکد نشده — که هر صفحهٔ billboardشده بالا می‌رود، به بیرون سر می‌خورد، پهن می‌شود و با فاز حلقهٔ خودش محو می‌شود. هر دو از الگوی گلبرگ‌های شناور پیروی می‌کنند: ماتریس‌های نمونهٔ همانی، attributeهای بذردار per نمونه روی زیرجریان‌های xorshift32 خودشان، همهٔ حرکت در گراف vertex از یک ساعت مقیاس‌یافتهٔ حرکتی، MeshBasicNodeMaterial با blending عادی و depth write خاموش، و یک کرهٔ مرزی صریح روی حجم جاروشده (الگوی gateway-motes).

پوشش گیاهی مدل‌محور (بیشهٔ مهتاب)#

بیشهٔ مهتاب نخستین zone پروژه است که هندسه‌اش به‌جای سازنده‌های رویه‌ای از فایل‌های .glb می‌آید. پنج پک با مجوز CC0 از Quaternius (environments/park/public/models/PACK-{BirchTree,DeadTree,Bush,Flower,Grass}.glb، مجموعاً ۱٫۵۴ مگابایت) از طریق هوک مشترک useGltfPack از @al-amr/r3f (ADR-0062) بار می‌شوند — decoderهای vendored Basis/Draco از origin خودِ محیط سرو می‌شوند (public/al-amr/decoders/ که با هوک predev/prebuild همگام می‌شود)، بارها با سقف ۲۰ ثانیه محدودند، کش per-renderer و با شمارش مرجع مدیریت می‌شود، و خطاها با AssetLoadError به‌عنوان خطای اپلیکیشن از مرز خطای رندر zone عبور می‌کنند. پک‌ها صفر انیمیشن و صفر skin دارند و همین است که instancing ساده را کافی می‌کند.

MoonGroveFlora.tsx هر variant استفاده‌شده (یک نود نام‌دار مثل BirchTree_3) را به یک InstancedMesh per primitive پک تبدیل می‌کند — جفت پوست+برگ غان دو draw است و بقیه یکی — و هندسه، مواد و بافت‌های KTX2 خودِ پک را بدون clone به اشتراک می‌گذارد؛ آزادسازی فقط بافرهای instance را رها می‌کند چون مالک بقیه کش پک است. کل سیستم در هر سطح ۲۸ draw است (غان ۱۰، درخت خشک ۵، بوته ۵، گل ۵، علف ۳)؛ کیفیت فقط جمعیت بذردار را کم و زیاد می‌کند. مرزها بعد از نوشتن ماتریس‌های instance دوباره محاسبه می‌شوند (computeBoundingSphere) تا frustum culling درست بماند، و یک تنوع بسیار ملایم روشنایی per نمونه از طریق instanceColor جلوی حک‌شده‌به‌نظررسیدن بیشه را می‌گیرد. بیشه به‌انتخاب هنری ساکن است — شبی آرام — پس این ماژول اصلاً useFrame ندارد.

پوشش گیاهی و باد#

پوشش گیاهی کاملاً instanced است: یک draw per دسته، در هر سطح. ایوان ابر سه گونهٔ درخت دارد — اکثریت بروادلیف تاج‌گرد، ستون باریک صنوبر و بید پهن و لایه‌ای، هر کدام با مش‌های instanced تنه و سقف و لنگرهای sway مخصوص خود، و اعضای هر لانه گونهٔ همان لانه را به ارث می‌برند — به‌علاوهٔ چمن و صخره‌ها (۸ فراخوان)؛ شکوفه‌زار مرجانی لانه‌های گلبرگ، گلبرگ‌های شناور و گل‌های چمن‌زار را اضافه می‌کند. تنوع tint، scale و کشش per نمونه از طریق attributeهای instanced پخته‌شده و InstancedMesh.instanceColor می‌رسد (که NodeMaterial بعد از colorNode در آن ضرب می‌کند، پس گراف هرگز به attribute مبتنی بر storage دست نمی‌زند). قاعدهٔ سخت‌آموخته که با تست پین شده: هر attribute اینستنس‌شده‌ای که یک شییدر TSL می‌خواند از همان ابتدا با تعداد کامل نمونه‌ها پخته می‌شود — یک attribute پخته‌نشده بی‌صدا سیاه رندر می‌کند. خودِ پشتهٔ سطح — گراف مادهٔ گرادیان/بافت‌ریز با جمله‌های اختیاری mottle/patch/rim/sunlit/moss، گره موقعیت بادخورده با گام چگالی، محو dithered نزدیک دوربین، ساعت دوربینِ گام throttleشده و نهایی‌سازی بادکردن مرزها — از scenes/vegetation/park-flora-surface.tsx بین هر دو zone مشترک است و با پالت، باندهای گام و ثابت‌های sway هر zone پارامتر می‌شود.

جنبش باد جابه‌جایی vertex در TSL است. میدان وزش سیار روی positionLocalِ تبدیل‌شده با ماتریس instance به‌علاوهٔ فاز flutter per نمونه (hash(strideId + phaseSalt))، بین لنگرهای swayBaseYM و swayTopYM گونه در فضای هندسه (positionGeometry.y) وزن می‌گیرد و نرمال می‌شود تا دامنه per نمونه مقیاس بگیرد؛ تنه و سقف یک جریان strideId مشترک دارند تا یک گیاه واحد با هم بجنبد و با هم رقیق شود. چون GPU رأس‌هایی را جابه‌جا می‌کند که CPU هرگز نمی‌بیند، مرزهای instanced از ماتریس‌های ایستا دوباره محاسبه و سپس به اندازهٔ *_MAX_WIND_DISPLACEMENT_M (۰٫۸۵/۰٫۹ متر) باد می‌شوند تا frustum culling در حین جنبش سقف‌ها درست بماند.

حذف چگالی-گام مبتنی بر فاصله، نمونه‌های دور را داخل شییدر حذف می‌کند و به‌روزرسانی uniform دوربین با آستانه throttle می‌شود (STRIDE_CAMERA_MIN_INTERVAL_S برابر ۰٫۲۵ ثانیه) و با hysteresis کنترل می‌شود (STRIDE_CAMERA_HYSTERESIS_M برابر ۵ متر). نمونه‌های حذف‌شده به‌جای انشعاب در شییدر به نقطه‌ای دور زیر جهان فرو می‌روند:

const keepHalf = step(mod(strideId, float(2)), float(0.5));
const keepQuarter = step(mod(strideId, float(4)), float(0.5));
const visible = inNear.add(inMid.sub(inNear).mul(keepHalf)).add(inFar.sub(inMid).mul(keepQuarter));
material.positionNode = mix(vec3(0, -500, 0), displaced, visible);

محو dithered نزدیک دوربین (discard به سبک screen-door، بدون شفافیت، پس مرتب‌سازی و blending دست‌نخورده می‌ماند) جلوی پرشدن لنز اول-person با فریم سیاه را می‌گیرد وقتی بازدیدکننده از پوستهٔ یک تنه یا سقف پایین عبور می‌کند. جزئیات سطحی پوست درخت و صخره از بافت‌های canvas رویه‌ای DOM-guarded می‌آید: میدان ارتفاع قطعی خالص-mثلثاتی که تفاضل‌های مرکزی Sobel‌اش نقشهٔ نرمال متناظر را اشتقاق می‌کند و یک بار per عمر منبع تولید می‌شود؛ مسیرهای تست headless و SSR مقدار null می‌گیرند و ماده به بافت رویه‌ای خالص خودش برمی‌گردد. هندسه‌های سقف با جوش‌دادن چندوجهی‌ها (mergeVertices)، جابه‌جایی رأس‌های مشترک با یک هش بذردار و محاسبهٔ مجدد نرمال‌های نرم ساخته می‌شوند — ورودی watertight، watertight می‌ماند، پس سیلوئت‌ها بدون ترک ارگانیک می‌شوند؛ تست‌های هندسه جعبهٔ مرزی هر سقف را با لنگرهای sway گونه پین می‌کنند.

بوت مرحله‌ای و بارگذاری#

mountکردن همهٔ ماژول‌های صحنه در یک commit، کامپایل همهٔ NodeMaterialها را در یک task همگام روی thread اصلی جمع می‌کند — همان فریز قابل‌مشاهدهٔ بوت. صحنهٔ هر zone به‌جای آن در هر فریم رندرشده یک مرحلهٔ بوت را از طریق useAfterRenderFrame mount می‌کند: اول آسمان + زمین، بعد آب + پورتال بازگشت، بعد پوشش گیاهی (سنگین‌ترین، تا کامپایل شییدرهایش در فریم خودش بماند). پیشرفت بوت per مرحلهٔ نقاشی‌شده به useEnvironmentBoot گزارش می‌شود و onReady فقط بعد از آن شلیک می‌شود که کل ترکیب واقعاً ۳ فریم نقاشی شده باشد — سیگنال آماده یعنی پیکسل‌های واقعی، نه کامپوننت‌های mountشده. refها حلقه را بدون تخصیص و در برابر replayهای StrictMode idempotent نگه می‌دارند. مالک رندر عمداً یک مرحلهٔ بوت نیست: در سطح بوم زندگی می‌کند و آسمان و زمینِ مرحلهٔ یک را در اولین فریم خودش نقاشی می‌کند.

بیشهٔ مهتاب یک پیچ‌وتاب اضافه می‌کند: مرحلهٔ پوشش گیاهی‌اش پنج پک glTF بار می‌کند، پس داخل مرز Suspense خودش mount می‌شود — آسمان و زمین مهتابی در حین رسیدن پک‌ها به نقاشی ادامه می‌دهند — و آمادگی zone به mountشدن واقعی بیشهٔ instanced گره خورده است، تا سیگنال آماده هرگز زودتر از مدل‌ها نیاید.

برخورد و زمین‌گیری حرکت#

همان تابع ارتفاعی که مش زمین را جابه‌جا می‌کند، rig حرکت را هم زمین می‌کند (navigation.sampleGroundHeight) و هر نشاندنی را هم روی زمین می‌نشاند — یک منبع حقیقت، پس پاها، گیاهان و مش هرگز با هم ناسازگار نمی‌شوند. برخورد ایستا فهرستی از دیسک‌های مانع است که per سطح کیفیت با resolveObstacles(quality) resolve می‌شود: دیسک‌های تنه و صخره که از همان جریان چیدمانی بازسازی می‌شوند که سطح فعال رندر می‌کند. این مهم است چون سطح‌های چیدمان عمداً prefix-nested نیستند (اهداف grove با تعداد درخواستی مقیاس می‌شوند)، پس فقط فهرست موانع هم‌سطح، برخورد را معتبر نگه می‌دارد — هر مانع قابل‌مشاهده مانع می‌شود و هیچ دیسک نامرئی‌ای هرگز مانع نمی‌شود و برخورد هرگز از LOD تزئینی پیروی نمی‌کند. سنگ‌ریزه‌های زیر ارتفاع زانو قابل‌عبور می‌مانند؛ برکه و رودخانه به دیسک نیازی ندارند چون کرانه‌هایشان به‌طراحی کم‌عمق و قابل‌گذرند.

چیدمان قطعی و بذرها#

هر *-model.ts در هر zone ریاضی خالص و بدون GPU است که بدون renderer تست واحد می‌شود. ارتفاع زمین از مثلثات به‌علاوهٔ ماسک‌های smoothstep ساخته می‌شود: تپه‌های غلتان که با ماسک شعاعی محو می‌شوند، صاف‌شدن صفای spawn، خیز لبه که چشم‌انداز را می‌بندد، و تراش‌های آب. ایوان ابر جهان دوسطحی را اضافه می‌کند — فلات بالایی که ۱۴ متر در یک باند blend پنج‌متری smoothstep بالا می‌رود، کانال رودخانه‌ای که در امتداد خط مرکزی سینوسی پیچ‌خم‌دار تراش می‌خورد، و ترفند امضایی: سطح آب در آبشار ناپیوسته افت می‌کند در حالی که کف کانال به‌طور پیوسته از گسست عبور می‌کند (بستر آبشاری تند زیر پرده)، پس میدان ارتفاع در صخره هرگز پاره نمی‌شود. حوضچهٔ سقوط حوض‌اش را با همان blendِ ماسک‌دارِ min می‌کند و یک لبهٔ خاکی کم‌ارتفاع دور هر دو تودهٔ آب حلقه می‌زند تا خط آب مثل کرانهٔ واقعی خوانده شود.

تصادف، یک xorshift32 بذردار با زیرجریان‌های مستقل per دستهٔ گیاهی است (seed ^ 0x7eee5 برای درختان، seed ^ 0x6a55 برای چمن، و الی آخر؛ خرده‌های شناور رودخانه و مه برخورد ایوان ابر از زیرجریان‌های خودشان می‌نوشند، seed ^ 0xf1ec5 و seed ^ 0x9157)، پس تغییر تعداد یک دسته، دستهٔ دیگر را هرگز دوباره بر نمی‌زند. پراکندگی یک حلقهٔ پذیرش با شبکهٔ فاصله است: groveها (سهمی از جمعیت) به‌علاوهٔ تکی‌ها، با یک cell-hash درشت (hashCell روی سلول‌های ۱۶متری با بذر خودش) که شاخص‌های رنگ را هدایت می‌کند تا درختان، لانه‌های گلبرگ و رگه‌های گل همسایه رنگ برگ و گلبرگ یکسانی بگیرند و در رگه‌های رنگی ادغام شوند. لانه‌های گلبرگ به درختان بذردار لنگر می‌شوند، پس گلبرگ‌های افتاده مثل ریزش شکوفه خوانده می‌شوند، نه کاغذرنگی تصادفی. محرومیت‌های چیدمان، حوضچهٔ برکه، کانال رود و کرانه‌ها، راهروی آبشار، صفای پورتال، صفای spawn و مسیر نمایشی جنوبی را باز نگه می‌دارند — اولین قدم‌ها به‌ساخت یک نمای بدون مانع است.

آزادسازی و حافظه#

هر منبع GPU — هندسه، ماده، pipeline، بافت — در یک useMemo ساخته و از طریق useDisposableRenderResource از @al-amr/r3f آزاد می‌شود، با پیاده‌سازی‌های idempotent برای dispose(). تعویض zone و تغییر سطح، ماژول‌های صحنه را remount می‌کند و سقف‌های بودجهٔ revisitGrowth (۱ هندسه، ۱ بافت) دقیقاً برای گرفتن نشت منابع GPU در صحنهٔ remountشده وجود دارند. گنبد آسمان هندسه و مادهٔ خود را هنگام unmount آزاد می‌کند؛ مه چشم‌انداز، scene.fog قبلی را برمی‌گرداند؛ مالک post مقدارهای قبلی toneMapping و toneMappingExposure رندرکننده را هنگام unmount برمی‌گرداند.

بودجه‌ها و راستی‌آزمایی#

هر zone بودجه‌های رندر تازه در قالب سطر مشترک PARK_RENDER_BUDGETS دارد (gardenWebGpuStandard، gardenWebGl2، gardenCompatible، revisitGrowth، به‌علاوهٔ سقف gzip جاوااسکریپت اولیه). سطرهای WebGL2 روی مسیر e2e رندرشده با رستر نرم‌افزاری Playwright در نمای spawn هر zone اندازه می‌شوند به‌علاوهٔ حدود ۱۵٪ حاشیه — شکوفه‌زار مرجانی ۳۰ فراخوان / ۹۵۵٬۹۰۰ مثلث اندازه‌گیری‌شده (بودجه ۳۸ / ۱٬۱۰۰٬۰۰۰)، ایوان ابر ۳۷ فراخوان / ۸۲۴٬۶۸۸ اندازه‌گیری‌شده به‌علاوهٔ drawهای تخمینی خرده‌های شناور و مه (بودجه ۴۵ / ۹۵۰٬۰۰۰) — و سطرهای WebGPU حاشیهٔ مسیربه‌مسیر را در انتظار اعتبارسنجی روی GPU واقعی نگه می‌دارند. سطرهای بیشهٔ مهتاب متفاوت تالیف شده‌اند و همین‌طور هم برچسب خورده‌اند: سقف‌های مثلث در خودِ چیدمان بذردار ریشه دارند (مجموع دقیق مثلث‌های هر variant پک per سطح — compatible برابر ۳۸۷٬۵۱۰ و standard برابر ۱٬۲۱۷٬۷۳۰ — که تست مدل آن را با GLBهای کامیت‌شده پین می‌کند) به‌علاوهٔ حاشیه، در حالی که سطرهای فراخوان، هندسه و بافت تخمین‌های ساختاری‌اند و اولین اجرای اندازه‌گیری‌شده روی مسیرهای e2e را منتظر می‌مانند. مدل‌های خالص برای قطعیت و نامتغیرها تست واحد می‌شوند؛ یک تست معماری، شعاع‌های جهانی هاردکدشده بیرون از world-constants.ts را ممنوع می‌کند (ماژول مدل خودِ هر zone همان نقش را برای جهانش بازی می‌کند)؛ و سفرهای e2e — ورود به zone، انتظار برای آماده، ثبت بودجه‌ها، بازگشت از پرده — زیر رستر نرم‌افزاری اجرا می‌شوند تا روی هر ماشینی پابرجا بمانند.

درس‌ها و ترفندهای قابل‌دزدیدن#

  • fragmentهای آسمان را با clip.z = clip.w به صفحهٔ دور پین کنید؛ دوربین را دنبال کنید و اول با depthWrite: false رندر کنید.
  • لهجه‌های HDR را از emissiveNode عبور دهید و فقط attachment امیسیوی MRT را bloom کنید — درخشش انتخابی بدون آستانه‌گذاری کل فریم.
  • حلقه‌های اکتاو نویز را در زمان ساخت گراف در JS باز کنید و FBM را بر مجموع دامنه‌ها نرمال کنید تا آستانه‌های پوشش در هر سطح یکسان خوانده شوند.
  • برای تغییر جهت هنری uniformها را جهش دهید؛ برای تعویض zone هرگز pipeline را بازسازی نکنید.
  • رنگ مه خطی را دقیقاً رنگ هالهٔ گنبد بگذارید تا لبهٔ جهان هرگز با آسمان درز نکند.
  • یک تابع ارتفاع، rig را زمین کند، مش را جابه‌جا کند و هر نشاندنی را بنشاند — هرگز سه نسخهٔ تقریبی نه.
  • سطح آب را ناپیوسته نگه دارید ولی کف کانال را پیوسته، تا میدان ارتفاع در آبشار هرگز پاره نشود.
  • هر اثر مجاور آب (حلقهٔ کف، مه برخورد، حلقهٔ drift) را به ریاضی رودخانهٔ خودِ zone لنگر کنید — هرگز مختصات جهانی را در شییدر هاردکد نکنید.
  • uv.v روبان را بر حسب مترِ جریان بنویسید تا قطعه‌های جدا آب با یک سرعت جهانی اسکرول شوند.
  • هر attribute اینستنس‌شده‌ای را که شییدر TSL می‌خواند با تعداد کامل نمونه‌ها بپزید — attributeهای پخته‌نشده بی‌صدا سیاه رندر می‌کنند.
  • مرزهای instanced را به اندازهٔ بیشینهٔ جابه‌جایی باد باد کنید تا frustum culling در برابر جابه‌جایی vertex در GPU دوام بیاورد.
  • نمونه‌های حذف‌شدهٔ گام را به‌جای انشعاب در شییدر به یک نقطه فرو ببرید و نزدیک دوربین به‌جای blending با dither محو کنید.
  • دیسک‌های برخورد را از همان جریان چیدمان هم‌سطحی resolve کنید که رندرکننده می‌کشد — برخورد هرگز نباید از LOD تزئینی پیروی کند.
  • هر variant پک glTF را per primitive اینستنس کنید و هندسه و مادهٔ پک را بی‌واسطه به اشتراک بگذارید — هیچ clone نکنید، فقط بافرهای instance را آزاد کنید و بعد از نوشتن ماتریس‌ها مرزهای instanced را دوباره محاسبه کنید.
  • در هر فریم رندرشده یک مرحلهٔ بوت mount کنید تا کامپایل شییدرها فریم بوت را فریز نکند؛ سیگنال آماده را فقط بعد از نقاشی‌شدن کل ترکیب شلیک کنید.