UnityChan(Channel)
559 subscribers
2.07K photos
86 videos
38 files
89 links
کانال آموزش و نکات UnityChan

مالک کانال : @FProSensei

حمایت مالی (دونیت) : https://daramet.com/FProSensei

گروه یونیتی https://t.me/UnityChan
Download Telegram
کد Custom Function برای شیدر گراف :


#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Refraction.hlsl"

void AniToonRefractionSS_float(
in float3 PositionWS, // Position node, فضای World
in float3 Normal, // نرمال موج‌دار -- همان که به SSR می‌دهی
in float3 ViewDir, // View Direction node, فضای World
in float4 ScreenPosition, // Screen Position, حالت Default
in float IOR, // ~1.333 برای آب
in float Thickness, // ضخامت بر حسب متر -- DepthFade * Depth Distance
in float Roughness, // 0 شفاف .. 1 تار
in float3 AbsorptionColor, // ضریب جذب به تفکیک کانال
in float AbsorptionStrength,
out float3 RefractedColor,
out float Weight)
{
#ifdef SHADERGRAPH_PREVIEW
RefractedColor = float3(0.0, 0.0, 0.0);
Weight = 0.0;
#else
float3 V = normalize(ViewDir);
float3 N = normalize(Normal);

float ior = max(IOR, 1.0001);
float thickness = max(Thickness, 0.0);

RefractionModelResult refraction =
RefractionModelBox(V, PositionWS, N, ior, thickness);

float2 hitNDC = ComputeNormalizedDeviceCoordinates(
refraction.positionWS, GetWorldToHClipMatrix());

float hitDeviceDepth = SampleCameraDepth(hitNDC);
float hitLinearDepth = LinearEyeDepth(hitDeviceDepth, _ZBufferParams);
float surfaceLinearDepth = LinearEyeDepth(PositionWS, GetWorldToViewMatrix());

float offsetMultiplier = (hitLinearDepth > surfaceLinearDepth) ? 1.0 : 0.0;
float2 samplingNDC = lerp(ScreenPosition.xy, hitNDC, offsetMultiplier);

float mipLevel = PositivePow(saturate(Roughness), 1.3)
* max((float)_ColorPyramidLodCount - 1.0, 0.0);

float3 color = SampleCameraColor(samplingNDC, mipLevel);
color *= GetInverseCurrentExposureMultiplier();

float3 absorption = exp(-max(AbsorptionColor, 0.0)
* max(AbsorptionStrength, 0.0)
* refraction.dist);
color *= absorption;

float2 coordCS = samplingNDC * 2.0 - 1.0;
float2 t = Remap10(abs(coordCS),
_SSRefractionInvScreenWeightDistance,
_SSRefractionInvScreenWeightDistance);
float edgeFade = Smoothstep01(t.x) * Smoothstep01(t.y);

RefractedColor = max(color, 0.0);
Weight = saturate(edgeFade);
#endif
}
FPro Sensei
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Refraction.hlsl"
خب توضیح خط به خط شروع میکنم بر خلاف GLSL ما در HLSL زبان شیدر نویسی مطرح C++ میتوانیم یک کتابخانه - Api یا هر چیزی که بشه ازش چیزی رو وارد کرد و از کلاس ها یا تابع هاش استفاده کرد رو به کار بگیریم متد #include همانند متد Import در پایتون هست که در مسیر :
 "Packages/com.unity.render-pipelines.core/ShaderLibrary/Refraction.hlsl" 


ماژول وارد کد نویسی ما میکند و ما حالا به کلاس و متد های داخلی ان درسترسی داریم ❗️
FPro Sensei
void AniToonRefractionSS_float(
متد void یک تابع برای ما تعریف میکند ریشه این متد از زبان C میاد که امروزه توی زبان های C++ - C# هم دیده میشه که استفاده از _float برای شیدر گراف مهمه یعنی شیدر گراف با این میشناسه که این قرار یک کاستوم نود باشع ❗️
🔥1
FPro Sensei
in float3 PositionWS, // Position node, فضای World
in float3 Normal, // نرمال موج‌دار -- همان که به SSR می‌دهی
in float3 ViewDir, // View Direction node, فضای World
in float4 ScreenPosition, // Screen Position, حالت Default
in float IOR, // ~1.333 برای آب
in float Thickness, // ضخامت بر حسب متر -- DepthFade * Depth Distance
in float Roughness, // 0 شفاف .. 1 تار
in float3 AbsorptionColor, // ضریب جذب به تفکیک کانال
in float AbsorptionStrength,
out float3 RefractedColor,
out float Weight)
این متغیر های ورودی هست که تعریف شده که اینظوری تعریف میشود :

A B C

A = از دو متد in و out میشه استفاده کرد

که اگر in باشد این متغیر ما ورودی هست یعنی داده میگیره و اگر out باشد باشد یعنی قراره خروجی بده

B = نوع داده را مشخص میکند برای مثال

float = عدد اعشاری که تک برداری هست

مثال = (1.3)

float2 = عدد اعشاری که بردار دو بعدی هست

مثال : (1.2658,2.565)

float3 = عدد اعشاری که بردار سه بعدی هست

برای مثال : (4.548,4.845.9)

و دیگر نوع داده : 2DUnityTexture و .....

C = اسم متغیر را تعریف میکنیم

برای مثال Normal

یک مثال کلی : in float3 PositionWS یعنی ورودی پوزیشین ابجکت را در فضای جهانی بگیر و out float3 RefractedColor یعنی خروجی شکست نور که در داخل شیدر محاسبه کردیم رو از نود بده بیرون
🔥1
FPro Sensei
#ifdef SHADERGRAPH_PREVIEW
این دو شرط #ifdef SHADERGRAPH_PREVIEW و #else تعریف شده بخاطر اینکه

میره ماکرو های تعریف شده رو بررسی میکنه اگر SHADERGRAPH_PREVIEW باشه داخل شرط اجرا میکنه نبود میره داخل else اجرا میکنه منظور از SHADERGRAPH_PREVIEW همون پیش نمایش شیدر هست
🔥1
FPro Sensei
RefractedColor = float3(0.0, 0.0, 0.0);
Weight = 0.0;
یک متعیر ساده تعریف میکنه که توضیح هم دادم چطوریه
🔥1
FPro Sensei
float3 V = normalize(ViewDir);
float3 N = normalize(Normal);
منظور از N نرمال سطح ابجکت و V همان زاویه دید دوربین هست که نرمالیز میکنه که مطمعن باشه خطا نباشه توی محاسبات
🔥1
FPro Sensei
float ior = max(IOR, 1.0001);
float thickness = max(Thickness, 0.0);
دو مقدار ior و thickness تعریف میکنه که با متد MAX میگه کمترین مقدار باید این باشه و زیر این نباید بره
🔥1
FPro Sensei
RefractionModelResult refraction =
RefractionModelBox(V, PositionWS, N, ior, thickness);
از همون تابع داخلی refraction استفاده میکنه و مقدایر (V, PositionWS, N, ior, thickness) میده به اون تا اون برام یک مدل RefractionModelBox برگشت بده و اما کار این تابع چیست :

بردار دید (V) را با نرمال سطح (N) و IOR شکست می‌دهد — refract(-V, N, 1/IOR) — یعنی زاویهٔ واقعی نوری که وارد آب می‌شود را حساب می‌کند.

فرض می‌کند سطح داخلی جسم (کف ظرف/کف آب) یک صفحهٔ موازی با سطح بالا و به فاصلهٔ thickness از آن است — یک «جعبه» ساده، نه انحنای واقعی.

پرتوی شکسته‌شده را با آن صفحهٔ داخلی تلاقی می‌دهد و نقطهٔ خروج (positionWS) و طول مسیر داخل ماده (dist) را برمی‌گرداند.
🔥1
FPro Sensei

float2 hitNDC = ComputeNormalizedDeviceCoordinates(
refraction.positionWS, GetWorldToHClipMatrix());
این خط نقطهٔ خروج پرتوی شکسته‌شده (که یک مختصات سه‌بعدی در فضای جهانی است) را به یک مختصات دوبعدی روی صفحهٔ نمایش (UV بین ۰ تا ۱) تبدیل می‌کند — یعنی «این نقطه از دنیای سه‌بعدی، دقیقاً کجای تصویر رندرشده قرار می‌گیرد؟» اما سوال این هست که چرا لازمه ؟

refraction.positionWS که از RefractionModelBox گرفتیم یک نقطه در فضای ۳بعدی جهان است (مثلاً «ته آب، ۲۰ سانتی‌متر پایین‌تر و کمی به یک طرف کج‌شده»). اما برای نمونه‌برداری از _ColorPyramidTexture (که یک تصویر ۲بعدی رندرشده از دوربین است) نیاز داریم بدانیم این نقطه در پیکسل‌بندی تصویر کجا می‌افتد. ❗️
FPro Sensei
float hitDeviceDepth = SampleCameraDepth(hitNDC);
float hitLinearDepth = LinearEyeDepth(hitDeviceDepth, _ZBufferParams);
float surfaceLinearDepth = LinearEyeDepth(PositionWS, GetWorldToViewMatrix());
خط اول: SampleCameraDepth(hitNDC)
در همان UV که از مرحلهٔ قبل (hitNDC) به‌دست آمد، به‌جای رنگ، مقدار عمق را از بافر عمق دوربین می‌خواند. خروجی‌اش یک عدد خام است (device depth، معمولاً بین ۰ و ۱، غیرخطی) — یعنی «چیزی که همین الان روی صفحه در این پیکسل رندر شده، چقدر از دوربین فاصله دارد؟»


خط دوم: LinearEyeDepth(hitDeviceDepth, _ZBufferParams)
عمق خام غیرخطی (device depth) را به یک عدد خطی به‌متر تبدیل می‌کند. بافر عمق در GPU معمولاً غیرخطی ذخیره می‌شود (دقت بیشتر نزدیک دوربین، کمتر دور) و برای مقایسهٔ مستقیم فاصله‌ها باید خطی‌اش کنیم. نتیجه: hitLinearDepth = «فاصلهٔ واقعی (متر) چیزی که در آن UV روی صفحه دیده می‌شود، از دوربین.»


خط سوم: LinearEyeDepth(PositionWS, GetWorldToViewMatrix())
این یکی overload متفاوتی از LinearEyeDepth است — به‌جای گرفتن device depth، مستقیم یک موقعیت world را می‌گیرد و با ماتریس world-to-view فاصلهٔ آن نقطه از دوربین را حساب می‌کند. اینجا PositionWS موقعیت خود سطح آب است (نه نقطهٔ شکسته‌شده). نتیجه: surfaceLinearDepth = «سطح آب خودش چقدر از دوربین فاصله دارد؟»
FPro Sensei
float offsetMultiplier = (hitLinearDepth > surfaceLinearDepth) ? 1.0 : 0.0;
float2 samplingNDC = lerp(ScreenPosition.xy, hitNDC, offsetMultiplier);
خط اول: offsetMultiplier
یک سوییچ صفر/یک می‌سازد:

اگر hitLinearDepth > surfaceLinearDepth (چیزی که پشت UV شکسته‌شده هست، از خود سطح آب دورتر است — یعنی واقعاً «پشت آب» است) → offsetMultiplier = 1.0
در غیر این صورت (چیزی که آنجاست جلوتر از آب است — یعنی یک جسم مثل پای کاراکتر جلوی آب ایستاده) → offsetMultiplier = 0.0

خط دوم: lerp(ScreenPosition.xy, hitNDC, offsetMultiplier)
بر اساس آن سوییچ، بین دو UV انتخاب می‌کند:

وقتی offsetMultiplier = 1.0 → نتیجه دقیقاً hitNDC می‌شود (همان UV شکسته‌شده که از RefractionModelBox + پروجکشن به‌دست آمد) — یعنی رفرکشن واقعی اعمال می‌شود.
وقتی offsetMultiplier = 0.0 → نتیجه دقیقاً ScreenPosition.xy می‌شود (UV خودِ پیکسل سطح آب، بدون هیچ آفستی) — یعنی رفرکشن خاموش می‌شود و همان‌جایی که هستیم را می‌خوانیم.

چون lerp با ضریب دقیقاً ۰ یا ۱ است (نه یک عدد میانی)، این عملاً یک if است نوشته‌شده به‌شکل بدون شاخه (branchless) — روی GPU این معمولاً سریع‌تر از if واقعی اجرا می‌شود.
🔥1
FPro Sensei

float mipLevel = PositivePow(saturate(Roughness), 1.3)
* max((float)_ColorPyramidLodCount - 1.0, 0.0);

float3 color = SampleCameraColor(samplingNDC, mipLevel);
color *= GetInverseCurrentExposureMultiplier();
خط اول: محاسبهٔ mipLevel
_ColorPyramidTexture فقط یک تصویر ساده نیست — یک mip chain است: نسخهٔ کامل‌رزولوشن (mip 0)، بعد نصف‌رزولوشن (mip 1)، ربع (mip 2)، و همین‌طور تا آخر. هر مرحلهٔ پایین‌تر عملاً یک نسخهٔ تارتر و کوچک‌تر از تصویر است

نتیجه: هرچه سطح زبرتر (Roughness بالاتر)، mipLevel بزرگ‌تر می‌شود → از سطح تارتر پیرامید نمونه گرفته می‌شود → رفرکشن محو/تار به‌نظر می‌رسد. سطح صاف/براق (Roughness=0) → mipLevel=0 → کاملاً واضح.

خط سوم: SampleCameraColor(samplingNDC, mipLevel)

از _ColorPyramidTexture در مختصات samplingNDC (همان UV نهایی که با گارد عمق تعیین شد) و در سطح mip محاسبه‌شده، رنگ را می‌خواند. این تابع wrapper است که خودش _ColorPyramidUvScaleAndLimitCurrentFrame را هم داخلش اعمال می‌کند (چون پیرامید رنگ همیشه دقیقاً هم‌اندازهٔ کل صفحه نیست، یک بخش کوچکش padding/unused است، و این ضریب scale/limit جلوی خواندن از آن ناحیهٔ نامعتبر را می‌گیرد).


خط چهارم: color *= GetInverseCurrentExposureMultiplier()
پیرامید رنگ، رنگ‌ها را به‌شکل pre-exposed ذخیره می‌کند — یعنی مقدار auto-exposure صحنه از قبل در آن ضرب شده.
🔥1
FPro Sensei

float3 absorption = exp(-max(AbsorptionColor, 0.0)
* max(AbsorptionStrength, 0.0)
* refraction.dist);
color *= absorption;

float2 coordCS = samplingNDC * 2.0 - 1.0;
float2 t = Remap10(abs(coordCS),
_SSRefractionInvScreenWeightDistance,
_SSRefractionInvScreenWeightDistance);
float edgeFade = Smoothstep01(t.x) * Smoothstep01(t.y);
بخش اول: جذب (Absorption) — قانون Beer-Lambert
absorption = exp(-AbsorptionColor * AbsorptionStrength * refraction.dist)
color *= absorption


هرچه refraction.dist بزرگ‌تر باشد (پرتو مسیر طولانی‌تری داخل آب طی کرده — یعنی آب عمیق‌تر)، exp(-...) کوچک‌تر می‌شود → رنگ بیشتر جذب/تیره می‌شود.

AbsorptionColor به هر کانال رنگ اجازه می‌دهد با نرخ متفاوت جذب شود — مثلاً آب معمولاً قرمز را سریع‌تر از آبی جذب می‌کند، برای همین آب عمیق آبی/سبز به‌نظر می‌رسد.

AbsorptionStrength یک ضریب کلی برای شدت این افکت است.

نتیجه: این نسخه از عمق واقعی مسیر نوری (refraction.dist) استفاده می‌کند — پس با عمق واقعی آب (نه فقط یک پارامتر ثابت) هماهنگ می‌شود.
🔥1
توضیح کامل قانون Beer - Lambert
🔥1👏1
FPro Sensei
float2 coordCS = samplingNDC * 2.0 - 1.0;
float2 t = Remap10(abs(coordCS),
_SSRefractionInvScreenWeightDistance,
_SSRefractionInvScreenWeightDistance);
float edgeFade = Smoothstep01(t.x) * Smoothstep01(t.y);

RefractedColor = max(color, 0.0);
Weight = saturate(edgeFade);
محاسبهٔ edgeFade
UV نمونه‌برداری را به بازهٔ [-1,1] می‌برد، فاصله تا نزدیک‌ترین لبهٔ صفحه را حساب می‌کند، و با Smoothstep01 یک مقدار نرم بین ۰ (دقیقاً روی لبه) تا ۱ (دور از لبه، وسط صفحه) می‌سازد. ضرب t.x * t.y یعنی اگر از هر جهت (افقی یا عمودی) به لبه نزدیک شویم، فید کم می‌شود.

RefractedColor = max(color, 0.0)
رنگ نهایی محاسبه‌شده (بعد از نمونه‌برداری از پیرامید، اصلاح exposure، و جذب) را در خروجی می‌گذارد. max(..., 0.0) فقط یک گارد ایمنی است — جلوی مقادیر منفی احتمالی (که در محاسبات نوری گاهی از خطای عددی float پیش می‌آید و در HDR/رندر بعدی مشکل ایجاد می‌کند) را می‌گیرد.

Weight = saturate(edgeFade)
مقدار fade را بین ۰ و ۱ محدود می‌کند و به‌عنوان خروجی دوم تابع (Weight) برمی‌گرداند.
🔥1👏1
در نهایت با ساخت و تعریف ورودی های نود میتوانید از این توی شیدر آبتون استفاده کنید آموزش تخصصی بود از جانب FPro Sensei ❗️
🔥1🥰1
FPro Sensei
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Refraction.hlsl"
جالب اینجاست که این com.unity.render-pipelines.core کتابخانه SRP هست یعنی مشترک بین URP و HDRP پس توی هر دو تاش میتونید استفاده کنید ولی URP فرق داره
🔥1🥰1
ویلبرورد اسنلیوس و کشف قانون شکست نور 🔍

که بود: ریاضی‌دان هلندی (۱۵۸۰–۱۶۲۶)، استاد دانشگاه لیدن، معروف به کار مثلث‌بندی برای اندازه‌گیری زمین 🌍📐.

کشف: با استفاده از تخصصش در زاویه و مثلثات، زاویهٔ تابش و شکست نور را در عبور از هوا به شیشه/آب اندازه گرفت 📏💧 و در سال ۱۶۲۱ فهمید نسبت سینوس این دو زاویه همیشه ثابت است — همان قانون امروزی:

n₁ sinθ₁ = n₂ sinθ₂ 📝

سرنوشت عجیب: خودش آن را منتشر نکرد 🤐. در ۱۶۲۶ درگذشت 🪦، دکارت در ۱۶۳۷ همین قانون را منتشر کرد 📢، و سال‌ها بعد هویگنس با بررسی دست‌نوشته‌هایش ثابت کرد اسنلیوس زودتر به آن رسیده بود 🕵️‍♂️ — برای همین قانون به نام او ماند 🏆.

نکته تاریخی: او هم اولین کاشف نبود؛ ابن سهل (ایرانی، حدود ۹۸۴ میلادی) 🇮🇷 و توماس هریوت هم پیش‌تر و مستقل به رابطه‌های مشابه رسیده بودند، ولی کارشان منتشر نشد 📜.

در نهایت تو در کار شیدر نویسی از این قانون دانشمند استفاده می‌کنی 👓

#دانستنی
🔥1🥰1