قبل از اینکه وارد کل حلقه شویم، اول در مورد اینکه Draw Call چیه صحبت کنیم. Draw Call چیزیه که CPU برای اینکه به GPU بگه چی رندر بکنه انجام میده. به نظر خیلی بزرگ و ترسناک میاد؛ چیزیه که دولوپر های جوان وقتی داستان تلاششون برای سریعتر کردن بازیشونو تعریف میکنن بکار میبرن!
یه Draw Call واقعی چیزی شبیه به اینه:
1. context->Draw(24, 0);
به همین سادگی. این فرمان یه مکعب با شیدری مشخص را می کشد و تمام اطلاعات لازم برای نمایش این مکعب بر روی صفحه را در خود دارد.
اما چگونه؟ در این فرمان فقط دو تا عدد هست، امکان نداره تمام اطلاعاتی که لازم داریم همینقدر باشه! درسته، کافی نیست. چون Draw Call به تنهایی فقط به این معنی هست که "خب، حالا با همه ی اطلاعاتی که از قبل ارائه داده ام صفحه را پر کن"
خب، حالا دوباره از ابتدا شروع کنیم. این تعریف دقیقا همان چیزی نیست که یونیتی انجام می دهد اما کمی از تصوری که قبلا داشته اید دقیق تر خواهد بود.
شما یک صحنه با یک دوربین دارید، در صحنه چندین مکعب، که همه یک شکل هستند وجود دارد، اما هر کدام از آنها شیدر و تکسچر متفاوتی دارد.
اولین فریم را رندر می کنیم:
سی پی یو:
1. رندرر ها: یونیتی کامپوننت های رندرری که در صحنه وجود دارد را چک می کند و همه را در یک لیست ذخیر می کند.
2. حذف کردن (Culling): یونیتی یک پس برای حذف کردن برخی رندرر ها انجام می دهد. هر مش یک Bounding Box دارد که به ازای هر رندرر تبدیل به یک AABB (Axis Aligned Bounding Box) شده است و یونیتی آنها را با frustum دوربین چک می کند. هر رندرری که Bound هایش در محدوده ی frustum نباشد از لیست حذف می شود. همچنین Layer Mask دوربین با Game Object رندرر مقایسه می شود و آنهایی که پنهان هستند از لیست حذف می شوند.
3. جمع آوری داده های Asset ها: یونیتی لیستی از مش ها، تکسچر ها و شیدر هایی را که رندرر های باقی مانده به آنها ارجاع می دهند را می گیرد. توجه داشته باشید که در اینجا به متریال ها اشاره ای نمی شود، اما متریال ها فقط راهی برای ارجاع به تکسچر، شیدر و لیستی از متغیر های دیگر هستند.
4. بارگذاری داده ها: یونیتی چک می کند که آیا این اولین بار است که هر کدام از این دیتا ها دیده شده اند یا نه. و اگر قبلا دیده نشده بودند آنها را بر روی GPU بارگذاری می کند. دیتای مرتبط با مش ها در اصل از چندین بخش تشکیل شده. مثلا یه مکعب، شامل آرایه ای از داده ها برای هر یک از 24 ورتکس است (شامل پوزیشن، یو وی ها، رنگ و غیره) و یک Index Array که شامل 36 اینتیجر است که آن 24 ورتکس را ایندکس می کنند، 3 تا برای هر triangles با توجه به اینکه برخی از triangles ها ورتکس مشترک دارند. بسیاری از این داده ها بعد از اینکه بر روی جی پی یو آپلود شدند از حافظه ی سی پی یو پاک می شوند.
5. مرتب سازی: در مرحله ی بعدی یونیتی کامپوننت های رندرر را مرتب سازی می کند. چندین بخش در این مرحله وجود دارد. render queue (صف رندر)، متریال ها، نورپردازی، فاصله و غیره. ما فعلا بر روی فاصله - که با کلمه ی عمق (Depth) هم بیان می شود - و مرتب سازی آبجکت های مات و شفاف تمرکز می کنیم و می گوییم که تمام متریال ها از یکی از Queue های مات یا شفاف برای مرتب سازی استفاده می کنند. آبجکت های مات همه در یک توده قرار می گیرند و از جلو به عقب مرتب می شوند. بر همین اساس آبجکت های نزدیکتر به دوربین طوری مرتب می شوند که در اول لیست قرار بگیرند. آبجکت های شفاف (مانند متریال شیشه ای) در توده ی دیگری قرار می گیرند و از عقب به جلو مرتب سازی می شوند؛ هر چه آبجکت ها دورتر باشند در ابتدای لیست قرار می گیرند.
6. آماده سازی رندرینگ: اینجا زمانی است که کار اصلی شروع می شود. تا بحال یونیتی دیتای Asset ها را بر روی جی پی یو آپلود کرده است و شاره گر هایی به جای رفرسنس مستقیم به آنها دارد. پس برای اینکه چیزی بر روی صفحه ی نمایشگر نقش ببندد فقط باید یگوید "هی، الان می خواهیم از این Asset استفاده کنیم." اما قبل از آن لازم است تا یکسری داده ی دیگر را که قرار نیست بین آبجکت های مختلف تغییر کنند را ستاپ کنیم. به طور کلی چیز هایی مثل تبدیل مختصات World به View (تبدیل از مختصات جهانی به مختصات دید دوربین) و ماتریس های Projection. این اطلاعات بین همه ی چیز های صحنه به اشتراک گذاشته می شود پس یکبار ستاپ می شوند و به جی پی یو فرستاده می شوند. همچنین در همینجا به GPU می گوییم که چه فریم بافر/ مقصد رندر چه باشد، یا اینکه از فریم قبلی پاک شود.
7. رندر آبجکت: پس از اینکه اطلاعات مشرک ستاپ شدند به جی پی یو اطلاعات منحصر به فرد هر آبجکت را می فرستیم و اینکار را از ابتدای لیستی که قبلا ساخته شده بود شروع می کنیم. این اطلاعات شامل: ماتریس انتقال object to world، هر گونه اطلاعات float، وکتور، پارامتر های رنگی که هر متریال دارد، چه تکسچری نیاز دارد و این تکسچر ها به کدام اینپوت متصل می شوند و اینکه از کدام مش استفاده بشود است. همچنین به جی پی یو گفته می شود که از کدام شیدر استفاده شود و چیزهای دیگری مثل Blend Mode، Front or Back face Culling، ZTest، ZWrite، و غیره نیز در این مرحله ستاپ می شوند. سپس می گوید "این را بر روی صفحه بکش". هر مقداری که از آبجت قبلا رندر شده تغییر نکند دست نخورده باقی می ماند و لازم نیست دوباره فرستاده شود.
8. آبجکت های مات: مرحله ی 7 آنقدر تکرار می شود تا لیست آبجکت های مات خالی شود.
9. اسکای باکس (Skybox): در این مرحله یونیتی مش اسکای باکس را رندر می کند که توسط یک Transform Matrix خاص طوری منتقل شده است که بینهایت دورتر از هر چیز داخل صحنه است. در واقع این مرحله هیچ فرقی با مرحله ی 7 ندارد، فقط بر روی اسکای باکس کامپوننت "رندرر" وجود ندارد.
10. آبجکت های شفاف: تکرار مرحله ی 7، اینبار برای لیست آبجکت های شفاف.
11. به جی پی یو گفته می شود که کارمان تمام شده و نتیجه را نشان بده.
تمام! البته بر روی سی پی یو. مراحل 1 تا 11 برای فریم بعدی تکرار می شود.
جی پی یو:
1. دارایی ها (Assets): در طول مرحله ی 4 از سی پی یو، جی پی یو اطلاعات آپلود شده را دریافت می کند و در داخل حافظه اش قرار می دهد. همین. از لحاظ فنی اشاره گر توسط بخش سی پی یو API گرافیکی برگشت داده می شود، که آنهم آنها را از بخش سی پی یو درایور های گرافیکی دریافت کرده که به جی پی یو می گوید "اینهم یکسری داده، در این بخش آدرس حافظه ذخیرشون بکن." جی پی یو فقط داده ها را می گیرد و پاسخ می دهد "اوه؛ باشه!"
2. داده های تغییر پذیر: در طول مرحله ی 6 سی پی یو، تقریبا همان مرحله ی 1 جی پی یو دوباره تکرار می شود. "اوه؛ باشه!". شبیه به مرحله ی 7 سی پی یو، به اضافه ی اینکه چندین سوئیچ تغییر کنند و یکسری دستورات صادر شوند. مقداری حافظه آماده شوند تا رندر انجام شود.
3. شروع Draw Call: شروع، یک مکعب را بر روی صفحه می کشیم.
3.1. ورتکس شیدر: جی پی یو چندین هسته دارد، چیزی بین ده ها تا چندین هزار تا. در بیشتر جی پی یو های امروزی همه ی این هسته ها هدف یکسانی دارند، حداقل وقتی داریم در مورد کدی حرف می زنیم که جی پی یو می تواند اجرا کند. پس برای یک مکعب با فقط 24 ورتکس، همه ی ورتکس ها بصورن موازی پردازش می شوند. موقعیت هر ورتکس از فضای "Local" (داده ای که در ساختمان (Struct) آرایه ی جی پی یو ذخیره شده) به فضای homogeneous clip space منتقل می شود. در همین مرحله ممکن است تغییراتی بر روی داده صورت گیرد یا اطلاعات بیشتری مانند UV ها و رنگ ورتکس ها هم منتقل شود - در صورتی که در شیدر کدی مربوط به آن آمده باشد و هر گونه اطلاعات متریال را که ممکن است در مرحله نیاز باشد را دریافت و پردازش کند.
3.2. رسترایزیشن: در مرحله ی بعدی جی پی یو آرایه ی ایندکس ها رفته و محدوده ی پوشش هر Triangle بر روی صفحه نمایش را بر اساس موقعیت ورتکس هایش محاسبه می کند. این مرحله به ترتیب از ابتدای آرایه انجام می شود و به صورت موازی نیست. اگر Triangle ای هیچ پیکسلی در صفحه را پر نکند در نظر گرفته نمی شود. اگر ZTest داشته باشد با depth buffer مقایسه می شود و پیکسل هایی که دیده نخواهند شد در نظر گرفته نمی شوند. اگر ZWrite داشته باشند، depth buffer با مقدار depth جدید آپدیت می شود. سپس پیکسل های مربوط به آن Triangle کشیده می شوند. برای هر پیکسل لازم است تا مختصات گرانیگاهی آن محاسبه شود و بین داده های ورتکس ها Interpolate (درون یابی) صورت گیرد تا به Fragment Shader ارسال شود.
3.3. فرگمنت شیدر: پیکسل های روی صفحه ی نمایش به صورت دسته ای پردازش می شوند. در بدترین حالت به صورت پیکسل های 2*2 که به صورت موازی پردازش می شوند. اما ممکن است این دسته ها به دسته های 8*8 یا 16*8 پیکسلی یا سایز های دیگر برسند. پیکسل ها در هر دسته به ازای هر Triangle کاملا به صورت موازی پردازش می شوند. - - . فرگمنت شیدر اطلاعات اینترپولیت شده را برای مختصات، متریال ها و تکسچر ها استفاده می کند و همه را به شکل یک مقدار رنگی یکی می کند. تکسچر ها به صورت سخت افزاری سمپل می شوند که باعث می شود دیکد کردن فرمت های مختلف تکسچر و گرفتن یک مقدار رنگی از آنها برای شیدر سریع باشد.
3.4. مقادیری که فرگمنت شیدر خروجی می دهد با فریم بافر ترکیب می شوند. برای چیز های مات (Opaque) معمولا اینطور است که کاملا جایگزین مقدار قبل بشوند. برای اجسام شفاف (Transparent) ممکن است مد های مختلف ترکیب شدن مانند alpha blending یا additive را داشته باشیم. جی پی یو سخت افزار مخصوصی برای اینطور عملیات دارد.
4. تمام بخش های مرحله ی سوم به ازای هر Draw Call -با توجه به ترتیبی که سی پی یو آنها را می فرستد- تکرار می شود.
5. تصویر نهایی موجود در فریم بافر به مانیتور فرستاده می شود.
فریم بعدی شروع می شود و همه چیز از ابتدا. توجه داشته باشید که تمام تکسچر ها، شیدر ها و اطلاعات مش ها همانطور در جی پی یو باقی می مانند و دوباره و دوباره مورد استفاده قرار می گیرند. بخش های کوچکی را نادیده گرفتیم، اما چکیده ی ماجرا همین است!