مناظر ابری رویایی در زمان واقعی با Raymarching حجمی

من چند ماه گذشته را صرف غواصی در قلمرو Raymarching و یادگیری برخی از کاربردهای آن کردهام که ممکن است برای پروژههای سه بعدی آینده مفید باشد، و در حالی که موفق به ساخت مجموعهای از صحنههای بسیار متنوع شدهام، همه آنها شامل رندر هستند. سطوح یا اجسام جامد. پست وبلاگ من در Raymarching برخی از قابلیتهای چشمگیر این تکنیک رندر را پوشش میدهد، و همانطور که در پایان پست ذکر کردم، این فقط نوک کوه یخ بود. وجود دارد خیلی بیشتر ما می توانیم با آن انجام دهیم.
یکی از جنبه های جذاب Raymarching که من به سرعت در تحقیقات خود با آن روبرو شدم، توانایی آن در تغییر برای ارائه بود. جلدها. به جای توقف چرخه مارش پرتو پس از برخورد پرتو به سطح، ما من فشار می دهم و این روند را برای تلاش ادامه دهید داخل از یک شی از آنجا بود که وسواس من با ابرهای حجیم شروع شد و فکر می کنم ساعت های بی شماری
گذراندن وقت برای کاوش در بسیاری از جزایر آسمان در Zelda Tears of the Kingdom کنجکاوی من را برای یادگیری بیشتر در مورد نحوه عملکرد آنها تقویت کرده است. بنابراین، من بسیاری از صحنههای شادرتوی مانند «ابرها» اثر اینیگو کویلز، «شب پرستاره» اثر الرو، و «نمونه حجمدهی ریمارچینگ» توسط سویوکو را با استفاده از تکنیکهای حجمی ریمارچینگ برای نمایش دود، ابرها و منظرههای ابری که مشخصاً نمیتوانم در برابر تلاش برای بازآفرینی مقاومت کنم، کاوش کردم.
من زمان زیادی را صرف تحقیق در مورد روشهای مختلف استفاده از Raymarching برای رندر کردن ابرها کردهام، از پیچیدن کامل سرم به اطراف. اصول اولیه Raymarching حجمی به استفاده از ویژگی های مبتنی بر فیزیکی ابرها سعی کنم خروجی واقعی تری داشته باشم در حالی که سعی می کنم تا حد ممکن عملکرد را از صحنه های منظم خود کم کنم نکاتی برای بهبود بهره وری در طول راه یاد گرفتم. من همه اینها را در این مقاله پوشش می دهم، که امیدوارم بتواند به عنوان راهنمای میدانی برای آزمایش های خود و یادگیری در مورد رندر حجمی باشد.
رندر حجمی: Raymarching با پیچ و تاب
در پست قبلی وبلاگ من در مورد Raymarching، دیدیم که این تکنیک به موارد زیر متکی است:
-
فیلدهای فاصله امضا شده: توابعی که فاصله یک نقطه معین در فضا را تا سطح یک جسم برمی گرداند
-
چرخه شروع مجدد جایی که ما راهپیمایی گام به گام همراه با پرتوهای ریخته شده از یک نقطه شروع (دوربین، چشم بیننده) از طریق هر پیکسل از تصویر منبعو فاصله تا سطح جسم را با استفاده از SDF محاسبه می کنیم. وقتی این فاصله به اندازه کافی کوچک شد، می توانیم یک پیکسل رسم کنیم.
اگر این تکنیک را در برخی از صحنه های خود تمرین کرده اید، خوش شانس هستید: Raymarching حجمی به همان اصول: دارای حلقه، پرتوهای پرتاب شده از مبدا و SDF است. با این حال، از آنجایی که ما به جای سطوح، حجم ها را رندر می کنیم، تغییر جزئی در تکنیک ایجاد می شود.
نحوه نمونه برداری از یک حجم
اولین باری که با مفهوم SDF آشنا شدیم، فهمیدیم که مهم است نه اینکه داخل بشی شی در طول حلقه Raymarching ما یک رندر زیبا داشته باشد. من حتی این واقعیت را در یکی از نمودارهایم که 3 نقطه را در مقابل یک شی نشان می دهد برجسته کردم:
-
P1دور از سطح قرار دارد، به رنگ سبز، نشان دهنده a مثبت فاصله تا سطح -
P2در فاصله نزدیک ε تا سطح قرار دارد، به رنگ نارنجی، -
P3قرار داده شده است داخل شی، به رنگ قرمز، نشان دهنده a است منفی فاصله تا سطح
وقتی حجم نمونه برداری می کنیم، در واقع مجبور خواهیم بود raymarch در داخل شی ما و نحوه تفکر خود در مورد SDF را دوباره فرمول بندی می کنیم: به جای نشان دادن فاصله تا سطح، اکنون از آن به عنوان تراکم از حجم ما
-
هنگام جستجوی مجدد در خارج، چگالی صفر است یا
0. -
هنگامی که ما یک تجدید نظر در داخل انجام دهیم، این مثبت است.
برای نشان دادن این شیوه جدید تفکر در مورد Raymarching در زمینه حجم، در اینجا یک نسخه اصلاح شده از ویجت است که در پست وبلاگم در مورد این موضوع در اوایل امسال معرفی کردم.
-0.5،0.5
0.5،0.5
-0.5،-0.5
0.5،-0.5
این فرمول بندی مجدد آنچه که یک SDF را تشکیل می دهد در نهایت دو اصل اساسی را در تکنیک Raymarching ما تغییر می دهد که باید در کد ما منعکس شود:
-
باید قدم به قدم با الف پیش برویم اندازه گام ثابت روی پرتوهای ما ما دیگر از مسافت برگشتی توسط SDF استفاده نمی کنیم.
-
SDF ما اکنون در حال پس دادن است مقابل فاصله تا سطح برای نشان دادن درست چگالی جسم ما (مثبت در داخل،
0بیرون)

اولین صحنه Volumetric Raymarching ما
اکنون که ما ایده ای از نمونه برداری از حجم ها با استفاده از آنچه در مورد Raymarching می دانیم داریم، می توانیم سعی کنیم آن را با اصلاح یک صحنه موجود پیاده سازی کنیم. برای اختصار، من جزئیات تنظیمات اولیه را توضیح نمی دهم
صحنه های شروع مجدد اگر میخواهید نقطه شروع خوبی داشته باشید، میتوانید به تنظیمات Raymarching من که قبلاً در مقاله قبلی معرفی کردهام بروید.
چیدمان صحنه کاملاً شبیه چیزی است که در Raymarching کلاسیک با آن آشنا هستیم. تغییراتی که باید انجام دهیم در موارد زیر یافت می شوند:
نمونه ای از SDF مورد استفاده در Raymarching حجمی
1
float sdSphere(vec3 p, float radius) {2
return length(p) - radius;6
float distance = sdSphere(p, 1.0);
خط Raymarching حجمی با اندازه گام ثابت
3
const float MARCH_SIZE = 0.08;7
vec3 p = rayOrigin + depth * rayDirection;11
for (int i = 0; i MAX_STEPS; i++) {12
float density = scene(p);18
p = rayOrigin + depth * rayDirection;
حالا یه سوال دیگه پیش میاد: پس از مثبت شدن چگالی ما برای نمایش حجم، چه چیزی را ترسیم خواهیم کرد؟
برای مثال اول، میتوانیم همه چیز را ساده نگه داریم و با آن بازی کنیم alpha کانال رنگهای ما تا آن را متناسب با چگالی حجم خود کنیم: هر چه جسم ما با رفتن به داخل آن متراکمتر شود، ماتتر/تیرهتر میشود.
یک حلقه بازخوانی حجم ساده
1
const float MARCH_SIZE = 0.08;3
vec4 raymarch(vec3 rayOrigin, vec3 rayDirection) {5
vec3 p = rayOrigin + depth * rayDirection;9
for (int i = 0; i MAX_STEPS; i++) {10
float density = scene(p);14
vec4 color = vec4(mix(vec3(1.0,1.0,1.0), vec3(0.0, 0.0, 0.0), density), density );16
res += color * (1.0 - res.a);20
p = rayOrigin + depth * rayDirection;
اگر بخواهیم این کد را در بوم React Three Fiber خود رندر کنیم، باید نتیجه زیر را دریافت کنیم.
ترسیم ابرهای پرزدار
ما اکنون اصول اولیه Raymarching حجمی را می دانیم و به کار می بریم. تا اینجای کار ما فقط یک کره حجمی ساده با چگالی ثابت را در حین عبور از حجم ارائه کردهایم که شروع خوبی است. اکنون میتوانیم از این صحنه ساده به عنوان پایهای برای انجام کارهای جالبتر استفاده کنیم: ابرها!
صدای بلند
انتقال از SDF ساده یک کره به یک ابر شامل ترسیم آن با آن است کمی سر و صدای بیشتر. ابرها هم شکل نیستند و نه چگالی یکسانی دارند، بنابراین باید مقداری تصادفی ارگانیک را از طریق نویز به حلقه Raymarching خود وارد کنیم. اگر برخی از مقالات قبلی من را خوانده اید، باید از قبل با مفهوم زیر آشنا باشید:
-
نویز، نویز پرلین و مشتق نویز ارزش
-
حرکت براونی فراکتال یا FBM.
-
نویز مبتنی بر بافت
برای تولید مناظر پرتو، ما از بافت نویز، مشتقات نویز و FBM برای به دست آوردن یک نتیجه ارگانیک دقیق استفاده کردیم. ما برای ایجاد تصادفی ارگانیک و به دست آوردن یک ابر از SDF خود به برخی از این مفاهیم تکیه خواهیم کرد.
عملکرد نویز برای چشم انداز Raymarched
4
vec2 u = f * f * (3. - 2. * f);6
float a = textureLod(uTexture, (p + vec2(.0,.0)) / 256.,0.).x;7
float b = textureLod(uTexture, (p + vec2(1.0,.0)) / 256.,0.).x;8
float c = textureLod(uTexture, (p + vec2(.0,1.0)) / 256.,0.).x;9
float d = textureLod(uTexture, (p + vec2(1.0,1.0)) / 256.,0.).x;11
float noiseValue = a + (b-a) * u.x + (c-a) * u.y + (a - b - c + d) * u.x * u.y;12
vec2 noiseDerivative = 6. * f * (1. - f) * (vec2(b - a, c - a) + (a - b - c + d) * u.yx);14
return vec3(noiseValue, noiseDerivative);
برای ابرها، مال ما noise عملکرد کمی متفاوت به نظر می رسد:
عملکرد نویز برای ابرهای حجمی
4
vec2 u = f * f * (3. - 2. * f);6
vec2 uv = (p.xy + vec2(37.0, 239.0) * p.z) + u.xy;7
vec2 tex = textureLod(uNoise,(uv + 0.5) / 256.0, 0.0).yx;9
return mix( tex.x, tex.y, u.z ) * 2.0 - 1.0;
راستش را بگویم، من این ویژگی را در بسیاری از دموهای شادرتوی دیدهام، بدون اینکه لزوماً اعتبار نویسنده یا حتی پیوندی به توضیحی را ببینم. من به استفاده از آن در طول کارم ادامه دادم، زیرا هنوز یک مدل نویز ابری متقاعد کننده تولید می کرد. در اینجا تلاشی برای جمع آوری برخی از ویژگی های آن از درک خودم است:
-
ابرها ساختارهای سه بعدی هستند، بنابراین عملکرد ما یک
vec3به عنوان ورودی: نقطه ای در فضا در ابر ما. -
جستجوی بافت با همتای منظر خود متفاوت است: ما آن را به عنوان یک برش دو بعدی از یک موقعیت سه بعدی در نظر می گیریم. را
vec2(37.0, 239.0) * p.zبه نظر من کمی تصادفی به نظر می رسد، اما از آنچه که من جمع آوری می کنم اجازه می دهد تا تنوع بیشتری در نویز حاصل وجود داشته باشد. -
سپس دو مقدار نویز را از جستجوی بافت خود بر اساس ترکیب می کنیم
zارزش برای تولید یک مدل نویز صاف و تغییر مقیاس آن در داخل[-1, 1]محدوده
اعمال این نویز همراه با یک حرکت براونی فراکتال کاملاً شبیه چیزی است که ما با مناظر Raymarched به آن عادت کرده ایم:
حرکت براونی فراکتال در صحنه بازخوانی حجم ما اعمال شد
2
vec3 q = p + uTime * 0.5 * vec3(1.0, -0.2, -1.0);9
for (int i = 0; i 6; i++) {10
f += scale * noise(q);20
float distance = sdSphere(p, 1.0);
اگر کد بالا را در نسخه ی نمایشی قبلی خود اعمال کنیم، به چیزی شبیه به یک ابر می رسیم 👀:
اضافه کردن نور
یک بار دیگر، ما تازه “شروع” می کنیم تا چیزی را ببینیم که به هدف ما نزدیک می شود، اما یک عنصر کلیدی را از دست می دهیم تا فضای ابری ما احساس بیشتری کند. ابری: نور.
دمویی که در قسمت قبل دیدیم فاقد عمق و سایه است و بنابراین در کل خیلی واقعی به نظر نمی رسد و این به دلیل عدم انتشار نور است.
برای افزودن نور به ابر خود و در نتیجه گرفتن سایه های بهتر، ممکن است بخواهیم همان نوری را که در صحنه های استاندارد Raymarching استفاده می کردیم، اعمال کنیم:
-
هنجار هر نقطه نمونه را با استفاده از ما محاسبه کنید
sceneتابع -
از حاصل ضرب مقیاس نور طبیعی و جهت نور استفاده کنید
نور پراکنده در صحنه Raymarched با استفاده از نرمال
1
vec3 getNormal(vec3 p) {4
vec3 n = scene(p) - vec3(15
vec3 ro = vec3(0.0, 0.0, 5.0);16
vec3 rd = normalize(vec3(uv, -1.0));17
vec3 lightPosition = vec3(1.0);19
float d = raymarch(ro, rd);23
vec3 color = vec3(0.0);26
vec3 normal = getNormal(p);27
vec3 lightDirection = normalize(lightPosition - p);29
float diffuse = max(dot(normal, lightDirection), 0.0);30
color = vec3(1.0, 1.0, 1.0) * diffuse;33
gl_FragColor = vec4(color, 1.0);
این در تئوری کار می کند، اما انتخاب بهینه برای Raymarching حجمی نیست:
-
را
getNormalاین تابع به نقاط نمونه زیادی برای تخمین ” گرادیان” در هر جهت نیاز دارد. در کد بالا به 4 عدد نیاز داریم، اما کدهایی وجود دارد که برای نتیجه دقیق تر به 6 نیاز دارند. -
خط حجم مجدد ما به منابع فشرده تر است: ما با اندازه گام ثابت در امتداد پرتو خود راه می رویم تا از چگالی حجم خود نمونه برداری کنیم.
بنابراین، برای نور پراکنده خود به روش یا تقریبی دیگری نیاز داریم. خوشبختانه Inigo Quiles تکنیکی را برای حل این مشکل در مقاله خود در مورد مشتقات جهت دار ارائه می دهد. به جای اینکه مجبور باشیم چگالی خود را در هر جهت مانند نمونه برداری کنیم getNormal، این روش با نمونه برداری از چگالی در نقطه نمونه برداری ما، مسئله را ساده می کند p و در افست در جهت نور و دریافت تفاوت بین این مقادیر به طور تقریبی تعیین چگونگی پراکندگی نور در حجم ما.

در نمودار بالا می بینید که ما در حال نمونه برداری از چگالی خود هستیم p1 و در مورد دیگر p1' این کمی پایین تر از پرتو است:
-
اگر چگالی در این مسیر افزایش یابد، به این معنی است که حجم متراکم تر می شود و نور بیشتر پراکنده می شود.
-
اگر چگالی کمتر شود، ابر ما ضخامت کمتری دارد و در نتیجه نور کمتر پراکنده می شود.
این روش تنها به 2 نقطه نمونهبرداری نیاز دارد و بنابراین به منابع کمتری نیاز دارد تا به ما تقریب خوبی از نحوه رفتار نور با حجم ارائه دهد. اطراف p1.
میتوانیم این فرمول پراکنده را به صورت زیر برای نمایش خود اعمال کنیم:
نور پراکنده با استفاده از مشتقات جهت
5
float diffuse = clamp((scene(p) - scene(p + 0.3 * sunDirection)) / 0.3, 0.0, 1.0 );7
vec3 lin = vec3(0.60,0.60,0.75) * 1.1 + 0.8 * vec3(1.0,0.6,0.3) * diffuse;8
vec4 color = vec4(mix(vec3(1.0, 1.0, 1.0), vec3(0.0, 0.0, 0.0), density), density );12
res += color * (1.0 - res.a);
این دوباره بسیار شبیه به کاری است که ما در Raymarching استاندارد انجام میدادیم، با این تفاوت که اکنون باید آن را روشن کنیم داخل حلقه Raymarching از آنجایی که ما یک حجم را نمونه برداری می کنیم و بنابراین باید محاسبه را چندین بار در کل حجم انجام دهیم زیرا چگالی می تواند متفاوت باشد، در حالی که یک سطح فقط به یک محاسبه روشنایی منتشر (روی سطح) نیاز دارد.
تفاوت ابر ما بدون نور و با نور پراکنده را در زیر مشاهده می کنید

و در اینجا نسخه آزمایشی حاوی مفهوم و کدی است که ما به تازگی ارائه کردیم 👀:
تبدیل ابرها
بیایید کمی استراحت کنیم تا صحنه خود را تغییر دهیم و با چیزهایی که تا کنون ساخته ایم کمی سرگرم شویم! علیرغم تفاوتهایی که بین Raymarching استاندارد و همتای حجمی آن وجود دارد، هنوز مفاهیم زیادی در رابطه با SDF وجود دارد که میتوانید هنگام ایجاد تصاویر ابری از آنها استفاده کنید.
می توانید سعی کنید یک ابر را به شکل های سرگرم کننده ای مانند صلیب یا چنبره تبدیل کنید، یا حتی بهتر، سعی کنید آن را در طول زمان از شکلی به شکل دیگر تبدیل کنید:
ترکیب SDF برای تبدیل ابرهای حجمی به اشکال مختلف
1
mat2 rotate2D(float a) {4
return mat2(c, -s, s, c);7
float nextStep(float t, float len, float smo) {8
float tt = mod(t += smo, len);9
float stp = floor(t / len) - 1.0;10
return smoothstep(0.0, smo, tt) + stp;15
p1.xz *= rotate2D(-PI * 0.1);16
p1.yz *= rotate2D(PI * 0.3);18
float s1 = sdTorus(p1, vec2(1.3, 0.9));19
float s2 = sdCross(p1 * 2.0, 0.6);20
float s3 = sdSphere(p, 1.5);21
float s4 = sdCapsule(p, vec3(-2.0, -1.5, 0.0), vec3(2.0, 1.5, 0.0), 1.0);23
float t = mod(nextStep(uTime, 3.0, 1.2), 4.0);25
float distance = mix(s1, s2, clamp(t, 0.0, 1.0));26
distance = mix(distance, s3, clamp(t - 1.0, 0.0, 1.0));27
distance = mix(distance, s4, clamp(t - 2.0, 0.0, 1.0));28
distance = mix(distance, s1, clamp(t - 3.0, 0.0, 1.0));
این دمو بازتولید این صحنه رندر حجمی شادرتوی است. من واقعاً این ساخته را دوست دارم زیرا نتیجه بسیار ارگانیک است و تصور ابر بودن را القا می کند نورد البته در شکل بعدی
همچنین می توانید سعی کنید به تصویر بکشید:
ترکیب های خلاقانه زیادی برای امتحان وجود دارد!
ممکن است متوجه شوید که اجرای صحنههایی که تاکنون ساختهایم میتواند باعث شود که رایانه شما مانند یک موتور جت با وضوح بالا به نظر برسد، یا حداقل تا آنجا که میتوانستند صاف به نظر نرسد. خوشبختانه، ما میتوانیم کاری در مورد آن انجام دهیم و از مقداری استفاده کنیم تکنیک های بهینه سازی عملکرد برای دستیابی به تعادل مناسب بین تعداد FPS و کیفیت خروجی.
اصطکاک نویز آبی
یکی از مشکلات اصلی عملکرد صحنه ابری پراکنده فعلی ما به این دلیل است:
-
تعداد مراحلی که برای نمونه برداری از حجم و حجم کم باید برداریم
marchSize -
برخی از محاسبات سنگین را که باید در حلقه خود انجام دهیم، مانند مشتق جهت یا FBM.
این مشکل تنها بدتر خواهد شد زیرا ما سعی می کنیم محاسبات بیشتری را برای دستیابی به نتیجه فیزیکی دقیق تری در قسمت بعدی این مقاله انجام دهیم.
یکی از اولین کارهایی که می توانیم برای تاثیرگذاری بیشتر این صحنه انجام دهیم این است که تعداد مراحل را کاهش دهید زمانی که از ابر خود نمونه برداری می کنیم و اندازه گام را افزایش دهید.با این حال، اگر این را روی برخی از نمونه های قبلی خود امتحان کنیم (از شما دعوت می کنم امتحان کنید)، برخی لایه بندی قابل مشاهده خواهد بود و حجم ما بیشتر شبیه نوعی سوپ شیری است تا یک ابر کرکی.

ممکن است قبلاً با مفهوم dithering یا برخی تصاویر با استفاده از سبک های dithering مواجه شده باشید.این فرآیند میتواند توهم رنگها یا رنگهای بیشتری را در تصویر نسبت به آنچه در دسترس است یا فقط برای اهداف هنری استفاده میشود، ایجاد کند.
توصیه می کنم اگر می خواهید GPU Dithering اثر Alex Charlton را بخوانید
من یک معرفی سریع می خواهم.
در Ray marching fog with blue noise، نویسنده نشان میدهد که چگونه میتوانید از پراکندگی نویز آبی در صحنهی پرتوهای خود برای حذف استفاده کنید. نوارها یا لایه بندی اثر به دلیل مراحل کمتر یا چرخه دانه بندی کمتر.
این تکنیک از یک الگوی نویز آبی استفاده میکند که الگوها یا گروههای کمتری نسبت به سایر نویزها دارد و کمتر برای چشم انسان قابل مشاهده است، تا هر بار که shader قطعه ما اجرا میشود، یک عدد تصادفی تولید میکند. سپس این عدد را به عنوان یک وارد می کنیم جبران در ابتدای حلقه raymard، نقطه شروع نمونه برداری در امتداد پرتو برای هر پیکسل از خروجی ما حرکت می کند.


سر و صدای آبی که به خط Raymarching ما تعدیل میکند
1
uniform sampler2D uBlueNoise;5
vec4 raymarch(vec3 rayOrigin, vec3 rayDirection, float offset) {7
depth += MARCH_SIZE * offset;8
vec3 p = rayOrigin + depth * rayDirection;14
float blueNoise = texture2D(uBlueNoise, gl_FragCoord.xy / 1024.0).r;15
float offset = fract(blueNoise);17
vec4 res = raymarch(ro, rd, offset);
با معرفی برخی اصطکاک نویز آبی در شیدر قطعه ما می توانیم این آرتیفکت ها را حذف کنیم و خروجی با کیفیت بالایی داشته باشیم در حالی که تعداد مراحل Raymarching را کم نگه داریم!
با این حال، تحت برخی شرایط، الگوی لرزش می تواند کاملا قابل توجه باشد. با نگاهی به چند نمونه دیگر Shadertoy، متوجه شدم که با وارد کردن a زمانی جنبه نویز آبی می تواند این مشکل را کاهش دهد.
خنثی کردن لرزش نویز آبی زمانی
1
float offset = fract(blueNoise + float(uFrame%32) / sqrt(0.5));
در اینجا مقایسه قبل و بعد از تک شات ما از ابر راهپیمایی پرتوی ما است. من حدس می زنم که نتایج در اینجا برای خود صحبت می کنند 😄.

و در اینجا نسخه ی نمایشی نشان می دهد که نویز آبی ما در حال تکان خوردن است و ابر نرم تری به ما می دهد ⛅:
ارتقاء مقیاس با فیلتر Bicubic
این بهبود دوم توسط @N8Programs هدف این است که برخی از مصنوعات نویز باقیمانده را که پس از معرفی نویز آبی در صحنه بازسازی ما باقی میمانند، برطرف کند.
فیلتر دو مکعبی در مقیاسبندی استفاده میشود و اجازه میدهد تا برخی از الگوهای نویز را صاف کند و در عین حال جزئیات را با محاسبه مقدار یک پیکسل جدید با نگاه کردن به 16 پیکسل مجاور از طریق چند جملهای مکعبی (منابع) حفظ کند.
من به اندازه کافی خوش شانس بودم که یک پیاده سازی فیلتر دو مکعبی Shadertoy که توسط خود N8Programs ساخته شده بود پیدا کردم! با این حال، اعمال آن به طور مستقیم در کار موجود ما چندان آسان نیست. ما باید این بهبود را به عنوان مرحله یا پاس خود در فرآیند رندر، تقریباً به عنوان یک اثر پس از پردازش، اضافه کنیم.
من یک راه آسان برای ساخت این نوع خط لوله را در مقاله خود با عنوان جلوههای زیبا و شگفتانگیز با WebGL Render Targets ارائه کردم، جایی که نشان میدهم چگونه میتوانید از Frame Buffer Objects (FBOs) برای اعمال افکتهای پس از پردازش در کل صحنه استفاده کنید که میتوانیم برای این مورد استفاده کنیم:
-
بوم اصلی خود را با rearching در a رندر می کنیم پورتال.
-
صحنه پیش فرض فقط حاوی یک است مثلث تمام صفحه.
-
صحنه اصلی خود را در یک هدف رندر رندر می کنیم.
-
ما در حال عبور هستیم صحنه پایه بافت هدف را به صورت یکنواخت ارائه می کند از رسانه فیلتر دو مکعبی ما.
-
ما از مواد فیلتر دو مکعبی به عنوان ماده برای مثلث تمام صفحه خود استفاده می کنیم.
-
فیلتر دو مکعبی ما صحنه پر سر و صدا شده ما را به عنوان یک بافت یکنواخت می گیرد و صحنه صاف شده را خروجی می دهد.

در اینجا یک مقایسه سریع از صحنه ما قبل و بعد از اعمال bicubic است
فیلتر کردن:

پیادهسازی کامل کمی طولانی است و شامل مفاهیمی است که قبلاً در پست وبلاگ متمرکز بر رندر مرور کردهام، بنابراین از شما دعوت میکنم در دمو زیر به آن نگاهی بیندازید:
استفاده از اهداف رندر به من این امکان را داد که بیشتر با وضوح صحنه اصلی رایمارش بازی کنم. میتوانید انتخابگر کوچکی را ببینید که به شما امکان میدهد انتخاب کنید که با چه رزولوشنی ابر rayarched ما را رندر کنید. می توانید متوجه شوید که تفاوت زیادی بین آنها وجود ندارد 1x و 0.5x که عالی است: ما می توانیم FPS بیشتری را بدون به خطر انداختن کیفیت خروجی بدست آوریم.
ابرهای دقیق فیزیکی
تا اینجا ما واقعاً توانسته ایم بسازیم مناظر زیبای ابری با Raymarching حجمی با استفاده از چند تکنیک ساده و ترکیب رنگ های مناسب. صحنه های به دست آمده به اندازه کافی راضی کننده هستند و توهم ابرهای بزرگ و متراکم را ایجاد می کنند، اما اگر بیشتر بخواهیم چه می شود واقع بینانه راه خروج
من زمان زیادی را صرف بحثها، ویدیوها و مقالاتی در مورد چگونگی حل مشکل ابرهای دقیق فیزیکی و تمام تکنیکهای مربوط به موتورهای بازی کردم. این یک سفر بوده است و میخواستم این بخش آخر را به این موضوع اختصاص دهم، زیرا موضوع را جذاب میدانم: از چند اصل فیزیکی ابرهای واقعی واقعی، میتوانیم ابرها را در WebGL با استفاده از Raymarching حجمی رندر کنیم!
قانون آبجو
من قبلاً مفهوم قانون آبجو را در پست وبلاگ Raymarching خود به عنوان راهی برای به تصویر کشیدن مه در دوردست یک صحنه معرفی کردم. بیان می کند که شدت نوری که از یک محیط شفاف می گذرد به طور نمایی با مسافتی که طی می کند مرتبط است. هر چه نور در محیط بیشتر منتشر شود، بیشتر جذب می شود. فرمول قانون بیر به شرح زیر است:
I = I0 * exp(−α * d)، کجا α ضریب جذب یا تضعیف است که توصیف می کند چقدر “ضخیم” یا “چگال” است. در دموهای خود ما به ضریب جذب نگاه خواهیم کرد 0.9، اگرچه من شما را تشویق می کنم که مقادیر مختلف را امتحان کنید تا بتوانید تأثیر این عدد را بر رندر نتیجه مشاهده کنید.

ما میتوانیم از این فرمول در کد GLSL خود استفاده کنیم و حلقه Raymarching را تغییر دهیم تا به جای هک شفافیت “هک” که در قسمت اول استفاده کردیم از آن استفاده کنیم:
استفاده از قانون بیر برای محاسبه و بازگرداندن انرژی نوری انباشته شده عبوری از ابر
2
#define ABSORPTION_COEFFICIENT 0.96
float BeersLaw (float dist, float absorption) {7
return exp(-dist * absorption);10
const vec3 SUN_POSITION = vec3(1.0, 0.0, 0.0);11
const float MARCH_SIZE = 0.16;13
float raymarch(vec3 rayOrigin, vec3 rayDirection, float offset) {15
depth += MARCH_SIZE * offset;16
vec3 p = rayOrigin + depth * rayDirection;17
vec3 sunDirection = normalize(SUN_POSITION);19
float totalTransmittance = 1.0;20
float lightEnergy = 0.0;22
for (int i = 0; i MAX_STEPS; i++) {23
float density = scene(p);26
float transmittance = BeersLaw(density * MARCH_SIZE, ABSORPTION_COEFFICIENT);27
float luminance = density;29
totalTransmittance *= transmittance;30
lightEnergy += totalTransmittance * luminance;34
p = rayOrigin + depth * rayDirection;
در قطعه کد بالا:
-
ما حلقه رایمارشینگ را از بین بردهایم، بنابراین اکنون به یک ویژگی مبتنی بر فیزیک بیشتر متکی است: قانون آبجو.
-
ما رابط تابع خود را تغییر دادیم: به جای برگرداندن یک رنگ کامل، اکنون a را برمی گرداند
floatنمایندگی مقدار نور یا انرژی نور عبور از ابر -
همانطور که از طریق حجم حرکت می کنیم، دریافتی را جمع می کنیم
transmittance. هر چه عمیق تر می شویم، نور کمتری اضافه می کنیم. -
دریافتی را برمی گردانیم
lightEnergy
نسخه ی نمایشی زیر نشان می دهد که استفاده از قانون آبجو در چرخه Raymarching ما چه می کند 👀
ابر حاصل کمی عجیب است:
همه اینها دوباره به دلیل عدم وجود الگوی نور مناسب است.
نور نمونه برداری
ابر جدید ما در حال حاضر با نور تعامل ندارد. می توانید سعی کنید تغییر کنید SUN_POSITION vector: رندر حاصل ثابت خواهد ماند. ما نه تنها به یک مدل روشنایی، بلکه به یک مدل دقیق فیزیکی نیز نیاز داریم.
برای این کار میتوانیم محاسبه کنیم که برای هر نقطه نمونه از چرخه Raymarching چقدر نور جذب میشود:
-
یک حلقه Raymarching تو در تو را اجرا کنید که از نقطه نمونه فعلی به منبع نور می رود (جهت نور)
-
نمونه ای از چگالی بگیرید و قانون بیر را همانطور که قبلاً انجام دادیم اعمال کنید
نمودار زیر این تکنیک را نشان می دهد تا درک آن کمی آسان تر شود:

قطعه کد زیر یکی از بسیاری از کاربردهای این تکنیک است. در ادامه از این استفاده خواهیم کرد:
یک حلقه تخصصی تودرتو برای نمونه برداری از نور دریافتی در یک نقطه نمونه برداری معین
2
#define MAX_STEPS_LIGHTS 63
#define ABSORPTION_COEFFICIENT 0.97
const vec3 SUN_POSITION = vec3(1.0, 0.0, 0.0);8
const float MARCH_SIZE = 0.16;10
float lightmarch(vec3 position, vec3 rayDirection) {11
vec3 lightDirection = normalize(SUN_POSITION);12
float totalDensity = 0.0;13
float marchSize = 0.03;15
for (int step = 0; step MAX_STEPS_LIGHTS; step++) {16
position += lightDirection * marchSize * float(step);18
float lightSample = scene(position, true);19
totalDensity += lightSample;22
float transmittance = BeersLaw(totalDensity, ABSORPTION_COEFFICIENT);26
float raymarch(vec3 rayOrigin, vec3 rayDirection, float offset) {28
depth += MARCH_SIZE * offset;29
vec3 p = rayOrigin + depth * rayDirection;30
vec3 sunDirection = normalize(SUN_POSITION);32
float totalTransmittance = 1.0;33
float lightEnergy = 0.0;35
for (int i = 0; i MAX_STEPS; i++) {36
float density = scene(p, false);40
float lightTransmittance = lightmarch(p, rayDirection);41
float luminance = density;43
totalTransmittance *= lightTransmittance;44
lightEnergy += totalTransmittance * luminance;48
p = rayOrigin + depth * rayDirection;
به دلیل این حلقه تودرتو، پیچیدگی الگوریتمی حلقه Raymarching ما به تازگی افزایش یافته است، بنابراین ما باید تعداد نسبتاً کمی از مراحل را برای نمونه برداری از نور خود تعریف کنیم، در حالی که چگالی کمتری را با کاهش تعداد اکتاوها در FBM محاسبه کنیم تا نرخ فریم مناسبی را حفظ کنیم (این کار آسانی است. پیروزی من برای جلوگیری از انداختن فریم های زیاد درخواست دادم).
تمام این ترفندهای کوچک و ملاحظات عملکرد در دمو زیر در نظر گرفته شده است:
پراکندگی ناهمسانگرد و تابع فاز
تا اینجا ما فرض کردیم که نور به طور یکنواخت در هر جهت پخش می شود که در میان ابر منتشر می شود. در حقیقت، نور به دلیل قطرات آب در جهات مختلف با شدت های مختلف پراکنده می شود. این پدیده نامیده می شود پراکندگی ناهمسانگرد (در مقابل همسانگرد زمانی که نور به طور یکنواخت پراکنده می شود) و برای داشتن یک ابر واقع گرایانه می توانیم سعی کنیم این را در حلقه Raymarching خود در نظر بگیریم.

برای شبیه سازی پراکندگی ناهمسانگرد در صحنه ابر ما برای هر نقطه نمونه برداری برای یک منبع نور معین، می توانیم از تابع فاز. رایج است تابع فاز Henyey-Greenstein، که تقریباً در تمام نمونه هایی که از لحاظ فیزیکی دقیق حجمی رایمارچینگ پیدا کردم با آن مواجه شدم.
اجرای GLSL این تابع فاز به شکل زیر است:
کاربرد تابع فاز Henyey-Greenstein
1
float HenyeyGreenstein(float g, float mu) {3
return (1.0 / (4.0 * PI)) * ((1.0 - gg) / pow(1.0 + gg - 2.0 * g * mu, 1.5));
اکنون باید نتیجه این تابع جدید را به حلقه Raymarching خود وارد کنیم، آن را در چگالی در یک نقطه نمونه معین ضرب کنیم، و چیزی که به دست میآوریم نورپردازی واقعیتر برای ابرمان است، به خصوص اگر منبع نور در حال حرکت باشد.
معرفی تابع فاز Henyey-Greenstein در حلقه Raymarching ما
1
float raymarch(vec3 rayOrigin, vec3 rayDirection, float offset) {3
depth += MARCH_SIZE * offset;4
vec3 p = rayOrigin + depth * rayDirection;5
vec3 sunDirection = normalize(SUN_POSITION);7
float totalTransmittance = 1.0;8
float lightEnergy = 0.0;10
float phase = HenyeyGreenstein(SCATTERING_ANISO, dot(rayDirection, sunDirection));12
for (int i = 0; i MAX_STEPS; i++) {13
float density = scene(p, false);17
float lightTransmittance = lightmarch(p, rayDirection);18
float luminance = density * phase;20
totalTransmittance *= lightTransmittance;21
lightEnergy += totalTransmittance * luminance;25
p = rayOrigin + depth * rayDirection;

دمو نهایی این مقاله در زیر صحنه ما را با موارد زیر نشان می دهد:
نتیجه واقعاً خوب به نظر می رسد، اگرچه من مجبور شدم یک اصطلاح ارزش اضافی را به فرمول انرژی نوری خود اضافه کنم تا زمانی که قسمت های متراکم در سایه هستند ابر “ناپدید” نشود.
یک مقدار اضافی به فرمول درخشندگی اضافه کرد
1
float luminance = 0.025 + density * phase;
نیاز به هک احتمالاً برخی از مشکلات کد من را برجسته می کند، به احتمال زیاد به دلیل نحوه استفاده من از مقدار انرژی نور حاصله که توسط حلقه Raymarching برگردانده شده است، یا یک ضریب جذب که کمی زیاد است. من مطمئن نیستم. اگر فرضیات اشتباه آشکاری در کد من پیدا کردید، لطفاً به من اطلاع دهید تا بتوانم ویرایش های لازم را انجام دهم.
برخی بهینهسازیهای دیگر نیز ممکن است تا ابر را نرمتر و متراکمتر نشان دهد، مانند استفاده از تقریب پودر آبجو (صفحه 64)، اما به من گفته شد که اینها فقط به دلایل زیباییشناختی استفاده میشوند و واقعاً مبتنی بر فیزیکی نیستند (همچنین من صادقانه بگویم که نمیتوانم بفهمم که چگونه آن را بدون تغییر قابل توجهی اجرا کنم. MAX_STEPS، MAX_STEPS_LIGHTS، و marchSize متغیرها 😅 و نتیجه هنوز عالی نبود).
@MaximeHeckel توجه داشته باشید که پودر آبجو یک تقریب غیر فیزیکی است. شاید ببینید:
https://t.co/LlHxW5x5sb
در این مقاله، چند روش برای رندر کردن حجمهای ابر مانند با Raymarching یاد گرفتیم و با توجه به اینکه چند هفته پیش حتی نمیتوانستم سرم را به مفهوم Volumetric Raymarching بپیچم، من از نتیجه راضی هستم و به خودم افتخار می کنم با توجه به اینکه این موضوع چقدر می تواند پیچیده و ترسناک باشد. من همچنین از توانایی اعمال برخی از اصول فیزیک و تکنیکهای رندر که معمولاً در تولیدات بازیهای ویدیویی سهگانه استفاده میشود در WebGL برای دستیابی به ابرهایی با ظاهر واقعی شگفتزده شدم.
با این کار، من هیجان زده هستم که ابرها را با راهپیمایی پرتو با زمین، مانند آنچه در مقاله Raymarching ارائه شده است، یا حتی چالشهای پیچیدهتر مانند ارائه یک سیاره با پراکندگی جوی واقعی را امتحان کنم. ایده دیگری که داشتم این بود که یک کهکشان پرتو بسازم. همانطور که میتوانیم آن را به یک ابر عظیم در فضا ساده کنیم و برخی از اصول فیزیک معرفیشده در این مقاله همچنان باید اعمال شوند و منجر به تجسمهای زیبا شوند.
امیدوارم این مقاله الهام بخش شما در تلاش های Raymarching شما باشد و کمک کرده باشد که این مفهوم ظاهراً درک نشدنی رندر حجمی کمی خوشامد آورتر شود 🙂 .



