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

پایپ‌لاین اَسِت‌های سه‌بعدی#

محیط‌های الامر عمدتاً رویه‌ای‌اند و به بودجه‌های عملکردی سخت‌گیرانه پایبندند. وقتی دنیایی یا افزونه‌ای واقعاً به مدل سه‌بعدی نیاز دارد — پوشش گیاهی، اشیای خاص، آواتار — آن مدل‌ها از این پایپ‌لاین عبور می‌کنند.

این مستند مسیر یک اَسِت سه‌بعدی را از فایل دانلودشده تا حافظهٔ GPU پوشش می‌دهد.

۱. پک‌کردن#

یک پکر بیشتر وجود ندارد، packages/cli/assets/pack-assets.mjs، و هر محیطی آن را به یک شکل اجرا می‌کند — چه دنیاهای اصلی داخل همین مخزن و چه پروژه‌ای مستقل که با npm create al-amr@latest ساخته شده است. دستور alamr create environment آن را با نام scripts/pack-assets.mjs داخل پروژهٔ تولیدشده کپی می‌کند.

این دستور هیچ آرگومان اجباری ندارد، چون ساختار پوشه‌ها خودِ قرارداد است:

npm run assets:pack

هر .glb یا .gltf داخل assets/models/<pack>/ به یک variant نام‌دار در public/models/PACK-<pack>.glb تبدیل می‌شود، و نام variant همان نام فایل منبع بدون پسوند است. فایل‌های منبع فقط خوانده می‌شوند: هیچ چیزی زیر assets/models/ ساخته، تغییر داده یا حذف نمی‌شود، چون برای مدلی که دانلود شده undo وجود ندارد. پکی که منابعش از زمان نوشته‌شدن خروجی تغییر نکرده باشد رد می‌شود؛ --force با این حال دوباره می‌سازدش.

هر پک جدولی از variantهایش چاپ می‌کند با تعداد primitive، تعداد مثلث و جعبهٔ مرزی برحسب متر، و --json همان اندازه‌گیری‌ها را به‌صورت دادهٔ ساختاریافته برای ایجنت چاپ می‌کند. جعبهٔ مرزی ستون اصلی این جدول است: مدلی که با واحد سانتی‌متر خروجی گرفته شده صد برابر کوچک‌تر می‌رسد و بی‌عیب — و نامرئی — رندر می‌شود.

پکر چه می‌کند#

  1. ادغام. همهٔ مدل‌های داخل پوشه در یک سند ادغام می‌شوند، پس N درخواست HTTP به یکی تبدیل می‌شود.
  2. حذف تکرار (dedup()). ده درخت یک مجموعهٔ طبیعت که همگی به Bark_Normal.png ارجاع می‌دهند به یک بافت جمع می‌شوند که یک بار آپلود می‌شود. دلیل اصلی وجود پک همین است: ده فایل .glb مستقل یعنی ده دانلود و — نیمهٔ گران‌ترش — ده بافت مجزای GPU، چون هیچ چیز به رندرر نمی‌گوید این‌ها یک تصویرند.
  3. پیوند (join()). primitiveهای هم‌متریال داخل یک variant ادغام می‌شوند، پس یک variant نمونه‌سازی‌شده به‌ازای هر متریال یک draw call هزینه دارد نه به‌ازای هر مشِ ساخته‌شده. این مرحله بعد از dedup/prune و پیش از weld/meshopt اجرا می‌شود، تا هرگز روی بافری که از قبل فشرده شده کار نکند.
  4. فشرده‌سازی بافت (KTX2 / Basis Universal). نقشه‌های base colour و emissive از ETC1S استفاده می‌کنند (ادراکی، بسیار فشرده)؛ نقشه‌های normal و ORM از UASTC (خطی، حافظ دادهٔ برداری). انکود کردن یک normal map با ETC1S آن را در فضای ادراکیِ رنگ کوانتیزه می‌کند، و normal map رنگ نیست بلکه یک میدان برداری است — نورپردازی لکه‌لکه می‌شود. بافت‌ها نسبت به ابعاد واقعی خودشان تغییر اندازه می‌دهند، نه نسبت به یک عدد جادویی.
  5. بهینه‌سازی متریال. --alpha-mask متریال‌های گران BLEND را به MASK تبدیل می‌کند و مرتب‌سازی از پشت به جلوی GPU را حذف می‌کند — که بیشترین اهمیتش برای پوشش گیاهی نمونه‌سازی‌شده است، جایی که مرتب‌سازی به‌ازای هر نمونه دقیقاً همان چیزی است که هندسهٔ شفاف را گزینهٔ بدی برای نمونه‌سازی می‌کند.
  6. فشرده‌سازی هندسه (Meshopt). بافرهای رأس و ایندکس با Meshoptimizer فشرده می‌شوند.

چرا محیط‌های اصلی آن را با مسیر صدا می‌زنند#

هر محیط "assets:pack": "node ../../packages/cli/assets/pack-assets.mjs" را اعلام می‌کند. هفت وابستگی پکر devDependencies ریشه‌اند و عمداً برای هر محیط دوباره اعلام نمی‌شوند، و به اسکریپت واسطی در ریشهٔ مخزن هم نیازی نیست.

این کار می‌کند چون Node ایمپورت‌های یک ماژول ES را از محل خود اسکریپت resolve می‌کند، نه از process.cwd(). جست‌وجو مسیر packages/cli/assets/node_modulespackages/cli/node_modulespackages/node_modules ← ریشهٔ مخزن را می‌پیماید، و هر هفت‌تا آنجا هستند. environments/<env>/node_modules هرگز خوانده نمی‌شود، پس لینک‌دهی سخت‌گیرانهٔ pnpm برای هر پکیج — که devDependencies ریشه را در اختیار پکیج ورک‌اسپیس نمی‌گذارد — اصلاً وارد ماجرا نمی‌شود. این با اجرای پکر در حالی که پوشهٔ کاری روی environments/park تنظیم شده بود تأیید شد: هر هفت‌تا resolve شدند، و هیچ‌کدام در node_modules خودِ آن محیط وجود ندارند.

نسخه‌ای که در پروژهٔ تولیدشده است به هیچ‌کدام از این استدلال‌ها نیاز ندارد: آن نسخه در scripts/pack-assets.mjs داخل پروژه‌ای می‌نشیند که خودش هر هفت وابستگی را اعلام می‌کند.

پک‌کردن هرگز به predev یا prebuild وصل نمی‌شود. یک build نباید دنبال assets/models/ بگردد یا انکودر بافت راه بیندازد؛ این دستور همیشه صریح است.

شش پکی که همین حالا در environments/park/public/models/ هستند با پکر قدیمی‌ترِ مخصوص مخزن ساخته شده‌اند که این پایپ‌لاین جایش را گرفت. منابع خام آن‌ها در مخزن نیست، پس دوباره ساخته نشده‌اند — بازسازی‌شان فقط بایت‌ها را جابه‌جا می‌کرد بی‌آنکه چیزی که منتشر می‌شود عوض شود.

۲. انتخاب منبع و آماده‌سازی#

تمام مدل‌های سه‌بعدی به‌کاررفته در مخازن اصلی باید CC0 (مالکیت عمومی) یا با مجوز سازگار باشند. ارائه‌دهندگانی مانند Quaternius منبع پرتکراری هستند.

  • فرمت مدل‌های منبع باید .glb یا .gltf باشد.
  • در مرحلهٔ تألیف از فرمت‌های از پیش بهینه‌شده یا مبهم پرهیز کنید.
  • مبدأ مدل (Pivot) باید در پایهٔ منطقی شیء باشد، به‌ویژه برای هر چیزی که قرار است نمونه‌سازی شود یا با فیزیک و شیدر باد حرکت کند.
  • برای اَسِت‌های ماژولار — درختی با مش‌های جدا برای تنه و تاج — ترنسفورم‌های محلی را نسبت به ریشهٔ اَسِت درست نگه دارید.

۳. ادغام در موتور: useGltfPack#

همان‌طور که در ADR-0079 آمده، از useGLTF کتابخانهٔ drei استفاده نمی‌شود: این هوک دیکودرها را از CDNهای بیرونی (gstatic، jsDelivr) می‌گیرد که هم میزبانی شخصی و هم بودجه‌های عملکردی را می‌شکند.

به‌جایش، هوک useGltfPack از @al-amr/r3f:

import { useGltfPack } from "@al-amr/r3f";

const pack = useGltfPack("/models/PACK-BirchTree.glb");
const variant = pack.getVariant("BirchTree_1");

نمونه‌سازی#

برای اینکه draw callها کمینه بمانند، هندسه و متریال‌ها را از پک بارگذاری‌شده بیرون بکشید و با InstancedMesh نمونه‌سازی کنید.

// Example: Creating an InstancedMesh from a variant
const instanced = new InstancedMesh(mesh.geometry, mesh.material, placementCount);

مالکیت و کلون‌کردن#

هرچه getVariant(name) برمی‌گرداند متعلق به کش پک است؛ کش مالک هندسه‌ها و بافت‌هاست و وقتی آخرین مصرف‌کننده unmount شود آزادشان می‌کند. پیش از آنکه وارد صحنه شود آن را با .clone(true) کلون کنید: هر Object3D دقیقاً یک والد دارد، پس mount کردن یک شیء variant در دو جا آن را بی‌صدا از جای اول حذف می‌کند — بدون خطا و بدون هشدار. از حدود بیست کپی از یک variant به بالا، دیگر کلون نکنید و نمونه‌سازی کنید.

مجموعهٔ کامل قواعدی که کد صحنهٔ تولیدشده باید رعایت کند رسپی ایجنت است؛ مسیر انسانی از دانلود مدل تا دیدنش در دنیا راهنمای مدل‌های سه‌بعدی است.

۴. ویرایش متریال با TSL و شیدرهای باد#

هنگام رندر پوشش گیاهی — درخت، چمن، بوته — جابه‌جایی باد روی GPU با زبان شیدرینگ Three.js (TSL) اعمال می‌شود.

مشکل ماتریس محلی#

یک شکست رایج هنگام اعمال جابه‌جایی رأس روی اَسِت‌های glTF، کش‌آمدن است یا اصلاً نبودِ حرکت. علتش این است که positionGeometry.y ارتفاع رأس را نسبت به مبدأ همان مش خاص اندازه می‌گیرد.

اگر آرتیستی برگ‌های درخت را به‌عنوان مشی جدا در y = 3.0 متری ساخته باشد، مقدار positionGeometry.y خود برگ‌ها باز از 0.0 شروع می‌شود. شیدر بادی که فرض کند 0.0 پایهٔ تنه است، هیچ بادی به برگ‌ها اعمال نمی‌کند.

راه‌حل: rootY#

ارتفاع مطلق رأس را نسبت به ریشهٔ کل اَسِت حساب کنید. ترنسفورم محلی زیرمش را استخراج و به سازندهٔ متریال TSL بدهید:

// 1. Extract the local offset and scale of the mesh relative to the variant root
const localPosition = new Vector3();
const localQuaternion = new Quaternion();
const localScale = new Vector3();
localMatrix.decompose(localPosition, localQuaternion, localScale);

// 2. Calculate the true height from the root
const rootY = positionGeometry.y.mul(float(localScale.y)).add(float(localPosition.y));

// 3. Calculate sway weight based on the true height
const swayWeight = pow(
  clamp(rootY.sub(float(swayBaseY)).div(float(swayTopY - swayBaseY)), 0, 1),
  float(swayPower),
);

جابه‌جایی حافظ قوس#

به‌جای جابه‌جایی خطی رأس‌ها در صفحهٔ XZ که هندسه را کش می‌آورد، یک افت درجه‌دوم در Y اعمال کنید تا طول قوس گیاهِ خم‌شونده حفظ شود:

const displaced = positionLocal.add(
  vec3(
    float(WIND_DIRECTION_XZ[0]).mul(amplitude),
    amplitude.mul(amplitude).mul(-0.5), // Quadratic drop preserves arc length
    float(WIND_DIRECTION_XZ[1]).mul(amplitude),
  ),
);
mat.positionNode = displaced;

آنگاه شاخ‌وبرگ بدون به‌هم‌ریختن تناسبات مش تکان می‌خورد.