کد 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)
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 یعنی خروجی شکست نور که در داخل شیدر محاسبه کردیم رو از نود بده بیرون
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 همون پیش نمایش شیدر هست
میره ماکرو های تعریف شده رو بررسی میکنه اگر SHADERGRAPH_PREVIEW باشه داخل شرط اجرا میکنه نبود میره داخل else اجرا میکنه منظور از SHADERGRAPH_PREVIEW همون پیش نمایش شیدر هست
🔥1
FPro Sensei
RefractedColor = float3(0.0, 0.0, 0.0);
Weight = 0.0;
Weight = 0.0;
یک متعیر ساده تعریف میکنه که توضیح هم دادم چطوریه
🔥1
FPro Sensei
float3 V = normalize(ViewDir);
float3 N = normalize(Normal);
float3 N = normalize(Normal);
منظور از N نرمال سطح ابجکت و V همان زاویه دید دوربین هست که نرمالیز میکنه که مطمعن باشه خطا نباشه توی محاسبات
🔥1
FPro Sensei
float ior = max(IOR, 1.0001);
float thickness = max(Thickness, 0.0);
float thickness = max(Thickness, 0.0);
دو مقدار ior و thickness تعریف میکنه که با متد MAX میگه کمترین مقدار باید این باشه و زیر این نباید بره
🔥1
FPro Sensei
RefractionModelResult refraction =
RefractionModelBox(V, PositionWS, N, ior, thickness);
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
از همون تابع داخلی refraction استفاده میکنه و مقدایر (V, PositionWS, N, ior, thickness) میده به اون تا اون برام یک مدل RefractionModelBox برگشت بده و اما کار این تابع چیست : بردار دید (V) را با نرمال سطح (N) و IOR شکست میدهد — refract(-V, N, 1/IOR) — یعنی…
اگر بازم متوجه نشدید که RefractionModelBox چیه با این تصویر کاملا متوجه میشید همون رابطه اسنل رو داره
🔥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());
float hitLinearDepth = LinearEyeDepth(hitDeviceDepth, _ZBufferParams);
float surfaceLinearDepth = LinearEyeDepth(PositionWS, GetWorldToViewMatrix());
خط اول:
در همان UV که از مرحلهٔ قبل (
خط دوم:
عمق خام غیرخطی (device depth) را به یک عدد خطی بهمتر تبدیل میکند. بافر عمق در GPU معمولاً غیرخطی ذخیره میشود (دقت بیشتر نزدیک دوربین، کمتر دور) و برای مقایسهٔ مستقیم فاصلهها باید خطیاش کنیم. نتیجه:
خط سوم:
این یکی overload متفاوتی از
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);
float2 samplingNDC = lerp(ScreenPosition.xy, hitNDC, offsetMultiplier);
خط اول:
یک سوییچ صفر/یک میسازد:
اگر
در غیر این صورت (چیزی که آنجاست جلوتر از آب است — یعنی یک جسم مثل پای کاراکتر جلوی آب ایستاده) →
خط دوم:
بر اساس آن سوییچ، بین دو UV انتخاب میکند:
وقتی
وقتی
چون
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();
خط اول: محاسبهٔ
نتیجه: هرچه سطح زبرتر (Roughness بالاتر)،
خط سوم:
از
خط چهارم:
پیرامید رنگ، رنگها را بهشکل pre-exposed ذخیره میکند — یعنی مقدار auto-exposure صحنه از قبل در آن ضرب شده.
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
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);
float2 t = Remap10(abs(coordCS),
_SSRefractionInvScreenWeightDistance,
_SSRefractionInvScreenWeightDistance);
float edgeFade = Smoothstep01(t.x) * Smoothstep01(t.y);
RefractedColor = max(color, 0.0);
Weight = saturate(edgeFade);
محاسبهٔ
UV نمونهبرداری را به بازهٔ
رنگ نهایی محاسبهشده (بعد از نمونهبرداری از پیرامید، اصلاح exposure، و جذب) را در خروجی میگذارد.
مقدار fade را بین ۰ و ۱ محدود میکند و بهعنوان خروجی دوم تابع (
edgeFadeUV نمونهبرداری را به بازهٔ
[-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θ₂ 📝
سرنوشت عجیب: خودش آن را منتشر نکرد 🤐. در ۱۶۲۶ درگذشت 🪦، دکارت در ۱۶۳۷ همین قانون را منتشر کرد 📢، و سالها بعد هویگنس با بررسی دستنوشتههایش ثابت کرد اسنلیوس زودتر به آن رسیده بود 🕵️♂️ — برای همین قانون به نام او ماند 🏆.
نکته تاریخی: او هم اولین کاشف نبود؛ ابن سهل (ایرانی، حدود ۹۸۴ میلادی) 🇮🇷 و توماس هریوت هم پیشتر و مستقل به رابطههای مشابه رسیده بودند، ولی کارشان منتشر نشد 📜❌.
در نهایت تو در کار شیدر نویسی از این قانون دانشمند استفاده میکنی 👓
#دانستنی
که بود: ریاضیدان هلندی (۱۵۸۰–۱۶۲۶)، استاد دانشگاه لیدن، معروف به کار مثلثبندی برای اندازهگیری زمین 🌍📐.
کشف: با استفاده از تخصصش در زاویه و مثلثات، زاویهٔ تابش و شکست نور را در عبور از هوا به شیشه/آب اندازه گرفت 📏💧 و در سال ۱۶۲۱ فهمید نسبت سینوس این دو زاویه همیشه ثابت است — همان قانون امروزی:
n₁ sinθ₁ = n₂ sinθ₂ 📝
سرنوشت عجیب: خودش آن را منتشر نکرد 🤐. در ۱۶۲۶ درگذشت 🪦، دکارت در ۱۶۳۷ همین قانون را منتشر کرد 📢، و سالها بعد هویگنس با بررسی دستنوشتههایش ثابت کرد اسنلیوس زودتر به آن رسیده بود 🕵️♂️ — برای همین قانون به نام او ماند 🏆.
نکته تاریخی: او هم اولین کاشف نبود؛ ابن سهل (ایرانی، حدود ۹۸۴ میلادی) 🇮🇷 و توماس هریوت هم پیشتر و مستقل به رابطههای مشابه رسیده بودند، ولی کارشان منتشر نشد 📜❌.
در نهایت تو در کار شیدر نویسی از این قانون دانشمند استفاده میکنی 👓
#دانستنی
🔥1🥰1