در مجموعه آموزش های Anime Shader (NPR) قراره یک سری از دوره های خاص برای متخصص شدن در طراحی محیط فوق العاده استلایزد و خاص یاد بگیرید برای اینکه دوره دنبال کنید در کانال BlenderHints عضو شوید ❗️
❤2
FPro Sensei
در مجموعه آموزش های Anime Shader (NPR) قراره یک سری از دوره های خاص برای متخصص شدن در طراحی محیط فوق العاده استلایزد و خاص یاد بگیرید برای اینکه دوره دنبال کنید در کانال BlenderHints عضو شوید ❗️
در واقعه این دوره ها یک دوره طلایی هستن تا برای هنرمندان واقعی که یاد بگیرد چطوری اون صحنه های که توی انیمه که فوق العاده زیبا و استلایز هست رو چطوری میسازن جواب همشو توی این کانال پیدا خواهید کرد 👆
❤4
FPro Sensei
در واقعه این دوره ها یک دوره طلایی هستن تا برای هنرمندان واقعی که یاد بگیرد چطوری اون صحنه های که توی انیمه که فوق العاده زیبا و استلایز هست رو چطوری میسازن جواب همشو توی این کانال پیدا خواهید کرد 👆
در این آموزش به شما توضیح خواهم داد چطوری میتوانید از شکست نور واقعی در HDRP استفاده کنید ولی قبل از اینکه توضیح بدم جطوری از API استفادع کنید در داخل شیدر گراف این جدول را بخوبی دقت کنید : (آموزش تخصصی شیدر نویسی از FProSensei)
🔥1
FPro Sensei
در این آموزش به شما توضیح خواهم داد چطوری میتوانید از شکست نور واقعی در HDRP استفاده کنید ولی قبل از اینکه توضیح بدم جطوری از API استفادع کنید در داخل شیدر گراف این جدول را بخوبی دقت کنید : (آموزش تخصصی شیدر نویسی از FProSensei)
اول از همه باید تفاوت روش های ایجاد Refraction را بدونید پس با دقت زیاد جدول رو مرور کنید
در مسیر پکیج اگر به این :
متوجه میشوید که در این مسیر شکست نور را محاسبه میکند خبر خوب اینکه این نه قفل مارک زد با Stencil دارد ونه اینکه حتما باید از Gbuffer استفاده بشه که توی Lit هست بلکه این توی بافر Forward+ هست پس براحتی میشه توی شیدر آب استلایزد استفاده بشه :
"Packages/com.unity.render-pipelines.core/ShaderLibrary/Refraction.hlsl"
متوجه میشوید که در این مسیر شکست نور را محاسبه میکند خبر خوب اینکه این نه قفل مارک زد با Stencil دارد ونه اینکه حتما باید از Gbuffer استفاده بشه که توی Lit هست بلکه این توی بافر Forward+ هست پس براحتی میشه توی شیدر آب استلایزد استفاده بشه :
کد 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