🌄 ارائه اسکای باکس Screen-space
توجه: این مقالهای نیست که به جزئیات بپردازد، بلکه بیشتر راهی برای مستندسازی نحوه پیادهسازی skybox در رندر من است. من از Metal استفاده می کنم، اما باید به API های دیگر تقریباً 1 به 1 ترجمه شود.
هنگامی که تصمیم گرفتم یک skybox را روی رندر خود پیاده کنم، اولین انگیزه من این بود که مقاله قابل اعتماد LearnOpenGL در نقشه های مکعبی را دنبال کنم، که اساسا شامل رندر کردن یک مکعب غول پیکر در اطراف دوربین است.
این یک رویکرد ساده و بسیار شهودی است اما من چیزی کمی خیال انگیزتر می خواستم.
همچنین، این تکنیک نیاز به بارگذاری دادههای زیادی دارد (همه رئوس مکعب، یک ماتریس نمای جدید تا زمانی که دوربین را حرکت میدهید در جای خود بماند و غیره) که در رویکرد فضای صفحهنمایش ضروری نیست.
📺 اجرای فضای صفحه نمایش
برای کسانی که با این اصطلاح آشنا نیستند، فضای صفحه اساساً به این معنی است که ما در یک صفحه (معمولاً یک چهارگوش یا مثلث غول پیکر) که کل صفحه را می پوشاند، رندر می کنیم.
اگر میخواهید فوقالعاده کارآمد باشید، میتوانید از یک مثلث غولپیکر استفاده کنید، اما من چهارگوش را انتخاب کردم، زیرا تنظیم آن سادهتر بود (من میتوانم موقعیتهای رأس خروجی را به عنوان گوشههای دستگاه نرمال شده مختصات فضایی در صفحه نزدیک را کد سخت کنم).
شیدر قطعه از بافت مکعب با جهت دید نمونه برداری می کند در فضای جهان.
برای بدست آوردن آن، من فقط عدد را ضرب می کنم راس توسط ماتریس های نمای معکوس و طرح ریزی قرار دهید و اجازه دهید GPU آن را درون یابی کند.
به طور معمول، در سایه زن راس از یک شی به جهان، به دوربین و در نهایت به فضای کلیپ/دستگاه می رویم، اما در این مورد ما از قبل مختصات دستگاه را داریم و به جهان نیاز داریم.
// So instead of doing:
device_space_pos = projection * view * model * objec_space_pos;
// We need the inverse transformation, which implies multiplying by the inverse
// matrices, in reverse order:
world_space_pos = inverse(view) * inverse(projection) * device_space_pos;
🚀 نکته عملکرد: معکوس کردن یک ماتریس یک عملیات گران است، اما ما می توانیم یک میانبر داشته باشیم.
ماتریسهای متعارف (ماتریسهایی که موقعیت نسبی بین نقاط را تغییر نمیدهند) دارای ویژگی مناسبی هستند که معکوس آنها همان جابهجایی آن است، که محاسبه آن بسیار آسانتر است.
خوشبختانه، ماتریس view متعارف است، بنابراین می توانیم مقداری کار را در آنجا ذخیره کنیم. با این حال، همین امر در مورد ماتریس طرح ریزی صدق نمی کند و باید به درستی معکوس شود.
ℹ️ میتوانید این ماتریسهای معکوس را بهجای محاسبه مجدد برای هر رأس، از پیش محاسبه کنید، اما از آنجایی که فقط ۴ راس وجود دارد (به طور بالقوه ۳)، هزینه آپلود و اتصال ماتریسهای اضافی احتمالاً بیشتر خواهد بود.
اگر اسکایباکس را اولین چیزی که در گذر میبینید رندر کنیم، همین خواهد بود. skybox هدف رندر را “پاک می کند” و هر چیز دیگری در بالای صفحه قرار می گیرد (فقط به یاد داشته باشید که نوشتن عمق را غیرفعال کنید).
با این حال من می خواستم بیشتر بروم و آن را کمی بیشتر بهینه کنم.
🥱 اجتناب از کار
من این ایده را از این پاسخ به یکی از توییت هایم دریافت کردم:
این واقعا منطقی است!
صحنه آزمایشی من در حال حاضر فقط 3 شی دارد: زمین، یک اسم حیوان دست اموز استنفورد و یک قوری یوتا، اما یک محیط “واقعی” چیزهای زیادی روی صفحه خواهد داشت و اسکای باکس تنها درصد کمی از رندر را می گیرد. هدف. چرا ما می خواهیم skybox را برای هر قطعه محاسبه کنیم، اگر اکثر آنها به هر حال بازنویسی می شوند؟ و به خاطر داشته باشید که ما در اینجا 2 عملیات گران قیمت انجام می دهیم: معکوس کردن یک ماتریس (مرحله راس) و نمونه برداری از یک نقشه مکعب (مرحله قطعه قطعه).
شابلون کاملا مناسب است.
در این مورد ما به هیچ چیز فانتزی نیاز نداریم، فقط باید بدانیم که آیا آن قطعه “رایگان” است یا خیر. بنابراین کاری که من انجام دادم این بود:
- تماس قرعه کشی skybox را به آن منتقل کنید پایان از صحنه عبور.
- برای هر شی صحنه، هنگام ترسیم یک قطعه، شابلون را افزایش دهید.
- هنگام رندر کردن skybox، فقط قطعاتی را بکشید که هنوز شابلون 0 دارند.
استنسیل من در انتهای پاس اینگونه به نظر می رسد:
سایهزن قطعه اسکایباکس فقط برای پیکسلهای سیاه اجرا میشود که باعث صرفهجویی زیادی در محاسبات میشود.
برای رسیدن به این هدف در کد باید:
- فرمت بافر Z را تغییر دهید تا شامل شابلون شود
- 2 حالت استنسیل جدا شده ایجاد کنید. یکی برای رندر صحنه و دیگری برای skybox.
mainStencilDesc.depthStencilPassOperation = .incrementClamp
mainStencilDesc.stencilCompareFunction = .always
// Only render the skybox for the fragments that have the same stencil value as the reference
skyboxStencilDesc.stencilCompareFunction = .equal
2.1 پاداش: نوشتن و آزمایش عمق را برای skybox غیرفعال کنید. 100٪ ضروری نیست زیرا چهارگوش آنقدر به دوربین نزدیک است که همیشه از آن عبور می کند، اما هزینه ای دارد که به راحتی می توانیم از آن اجتناب کنیم.
skyboxDSDesc.frontFaceStencil = skyboxStencilDesc
skyboxDSDesc.isDepthWriteEnabled = false
skyboxDSDesc.depthCompareFunction = .always
- حالات را عوض کنید و مقدار مرجع را تنظیم کنید
...
// All other scene draw call encoding
...
cmdEncoder.setDepthStencilState(skyboxDepthStencilState)
cmdEncoder.setStencilReferenceValue(0);
// Encode the skybox draw call
...
و شما بروید!
🧑🏫 نتیجه گیری
😄 خوبی ها
- بسیار ساده برای پیاده سازی.
- هزینه پهنای باند بسیار کم: این رویکرد فقط ما را ملزم میکند که نقشه مکعب و 4 راس (اگر از مثلث استفاده میکردیم 3) بارگذاری کنیم که واقعاً نیازی به حمل اطلاعات ندارند. ما حتی میتوانیم فقط یک نقطه را آپلود کنیم و چهار را در مرحله هندسه تولید کنیم. نقشه مکعب می تواند (و خواهد شد) برای تکنیک های دیگر استفاده شود، بنابراین واقعاً هزینه اضافی ندارد و می تواند محدود بماند، از همان نمایش و ماتریس های طرح ریزی استفاده می کند، بنابراین نیازی به پیوند دادن نداریم، و از همان پیوست ها استفاده می کند. بقیه صحنه تا بتواند در همان پاس بماند.
- گران ترین قسمت (نمونه برداری نقشه مکعبی) فقط در جایی انجام می شود که به شدت ضروری باشد.
😭 بدی ها
- به اندازه رویکرد مکعب غول پیکر “شهودی” نیست؟
👹 زشت
- من بیشتر از آنچه که به اعتراف به اشکال زدایی “تحریف لنز” و شک در دانش ریاضی و گرافیک خود افتخار می کنم، وقت صرف کردم. معلوم شد که من ماتریس پروجکشن را با جابهجایی آن، با وجود متعارف نبودن، «معکوس» کردم.
- مدیریت وضعیت استنسیل عمقی می تواند در برخی از API های گرافیکی آزاردهنده باشد؟