مقالات

درباره به تصویر کشیدن آسمان، غروب خورشید و سیارات


این عکسی وجود دارد که مدتی است روی تخته الهام من نشسته است، از شاتل فضایی اندیور که در فضا در مدار پایین زمین در غروب آفتاب معلق شده است. این اتمسفر بالای زمین را به عنوان پس‌زمینه نشان می‌دهد که دارای لایه‌های زیبا و رنگارنگی است که از نارنجی عمیق تا آبی قبل از محو شدن در سیاهی عمیق فضا متغیر است. نه تنها این گرادیان رنگ از نظر زیبایی شناسی دلپذیر است، بلکه پدیده ای که در پشت این رنگ ها قرار دارد، پراکندگی اتمسفر، وقتی شروع به بررسی نحوه عملکرد و نحوه بازتولید آن کنید، موضوع جالب‌تری است.

شاتل Silhouette https://www.nasa.gov/image-article/shuttle-silhouette-2/

من می‌خواستم نسخه‌ای از این افکت را با سایه‌زن‌ها بسازم و رنگ آبی متمایز آسمان و غروب‌ها و طلوع‌های واقعی خورشید را مستقیماً در مرورگر ارائه کنم. هدف این بود که تا حد امکان به این عکس نزدیک شویم و در عین حال به سمت نوعی رندر جوی حرکت کنیم که اغلب در بازی ها و سایر رسانه های مبتنی بر سایه زن دیده می شود.

در اینجا مجموعه ای از آنچه از آن سفر یک ماهه به دست آمد، در زمان واقعی ارائه شده است:

من در ابتدا قصد نداشتم در مورد این موضوع بنویسم، اما هیجان پیرامون ماموریت اخیر Artemis II، همراه با علاقه شخصی من به همه چیز در فضا، ارزش کاوش عمیق را داشته باشد. همچنین به نظر می‌رسد فرصتی عالی برای ایجاد یک تجربه تعاملی است که می‌تواند موضوع را در دسترس‌تر کند.
در این نوشته خواهیم دید نحوه اجرای گام به گام اثر سایه زن پراکنده جوی پس از پردازش، با اجرای بلوک های مختلف ساختمانی (ریمارچینگ، پراکندگی رایلی و می و همچنین جذب ازن) به تصویر کشیده می شود. گنبد آسمان واقعیو سپس نتیجه را برای ارائه آن به صورت تطبیق دهید یک پوشش جوی در اطراف یک سیاره.
در نهایت، ما رویکرد مبتنی بر LUT سباستین هیلر را برای یک نتیجه کارآمدتر، یا حداقل من، بررسی خواهیم کرد. تجربه در کاربرد آن، همانطور که تا حد زیادی بود من از منطقه راحتی خود خارج می شوم فاز برای این پروژه

ممکن است زمانی سعی کرده باشید که پس‌زمینه‌ای با گرادیان آبی در پشت برخی از کارهای خود قرار دهید تا ظاهری «جویانه‌تر» به آن ببخشید و آن را یک روز بنامید، اما به‌سرعت متوجه شدید که انجام این کار هرگز درست نیست. 1.

برای اجرای واقعی تر، ما باید آسمان و رنگ آن را به عنوان نتیجه برهمکنش نور با هوا و اجزای آن است، ضمن در نظر گرفتن چندین متغیر از جمله ارتفاع ناظر، میزان گرد و غبار، زمان روز و غیره، همه در حجم.

با مشخص شدن این موضوع، هدف ما برای این بخش اول استفاده از آن به عنوان یک اصل راهنما برای ایجاد زمینه ای برای سایه بان اتمسفر و رسیدن به نتیجه ای است که تقریباً در هر زمانی از روز از یک آسمان واقعی قابل تشخیص نیست.

نمونه برداری چگالی جوی

مشابه نحوه نزدیک شدن به ابرهای حجیم یا نور حجیم، یک راه آسان برای نمونه برداری از جو است راهپیمایی مجدد. می‌توانیم پرتوهایی را از موقعیت دوربین در صحنه پرتاب کنیم و از محیط شفاف عبور کنیم تا به دو سؤال زیر پاسخ دهیم:

  1. چه مقدار نور از طریق جو زنده می ماند؟ همین است نفوذپذیری مدت

  2. در هر نمونه چقدر نور به دوربین هدایت می شود؟ همچنین به عنوان شناخته شده است حواس پرتی.

سایه‌زنی که در این بخش اول ایجاد کردیم، کادرهای زیادی را بررسی می‌کند، اما در حال حاضر فقط یک پس‌زمینه صاف داریم. اگر بخواهیم از آن در یک صحنه React Three Fiber در حالت فعلی استفاده کنیم، فقط یک پس‌زمینه خوب برای صحنه‌های خود خواهیم داشت و نه بیشتر.

در این بخش ما شیدر تخت خود را به یک شیدر مناسب تبدیل می کنیم اثر پس از پردازشکه به ما اجازه می دهد جو را به صورت زیر نمایش دهیم:

  • حجم و عمق صحنه در طول مسیر را با بازسازی مختصات فضایی جهان از screenUV مختصات

  • پوسته ای در اطراف یک شبکه سیاره ای.

بازسازی فضای جهان، عمق و مه جوی

برای اعمال تاری اتمسفر در یک صحنه، ما فقط یک آسمان را نقاشی نمی کنیم. ما باید فضای بین دوربین و اشیاء مختلف نشان داده شده روی صفحه را پر کنیم. ما خوش شانس هستیم که قبلاً بخشی از این کار را در بخش اول انجام داده ایم: ما تمام داده های چگالی مورد نیاز برای محاسبه را داریم. چیزها در حجمی که صحنه سه بعدی ماست. تنها چیزی که در اینجا لازم است این است:

  1. یک افکت پس از پردازش ایجاد کنید که بتواند آسمان ما را سایه بان کند.

  2. بافر عمق را در صحنه ما و دوربین دریافت کنید projectionMatrixInverse، matrixWorld، و position، آنها را به عنوان یکنواخت اثر منتقل کنید.

  3. با تبدیل مختصات فضای صفحه به مختصات فضای جهان با تابع زیر، پرتوهای سه بعدی دوربین خود را از طریق هر پیکسل از جلوه خود بازسازی کنید:

تابع getWorldPosition

1

vec3 getWorldPosition(vec2 uv, float depth) {

2

float clipZ = depth * 2.0 - 1.0;

3

vec2 ndc = uv * 2.0 - 1.0;

4

vec4 clip = vec4(ndc, clipZ, 1.0);

6

vec4 view = projectionMatrixInverse * clip;

7

vec4 world = viewMatrixInverse * view;

9

return world.xyz / world.w;

اکنون که می دانیم چگونه به دست آوریم worldPosition از پیکسل فعلی، می توانیم:

انجام این کار تضمین می کند که خط raymarch ما اکنون در امتداد a پیش می رود پرتو سه بعدی.

1

float depth = readDepth(depthBuffer, uv);

2

vec3 rayOrigin = uCameraPosition;

3

vec3 worldPosition = getWorldPosition(uv, depth);

4

vec3 rayDir = normalize(worldPosition - rayOrigin);

آخرین کاری که اکنون باید انجام دهیم این است که مرور مجدد ما هر هندسه صحنه را در نظر بگیرد. برای انجام این کار، از بافر عمق صحنه خود برای تعریف raymark خود استفاده می کنیم stepSize به جای اینکه از یک ثابت استفاده کنیم تا بتوانیم نقاط نمونه خود را متناسب با پرتویی که در حال حاضر روی آن راه می رویم قرار دهیم.

1

float depth = readDepth(depthBuffer, uv);

2

vec3 rayOrigin = uCameraPosition;

3

vec3 worldPosition = getWorldPosition(uv, depth);

4

vec3 rayDir = normalize(worldPosition - rayOrigin);

6

float sceneDepth = depthToRayDistance(uv, depth);

10

float SKY_MARCH_DISTANCE_MULTIPLIER = 8.0;

12

bool isBackground = depth >= 1.0 - 1e-7;

17

sceneDepth = atmosphereHeight * SKY_MARCH_DISTANCE_MULTIPLIER;

21

float rayEnd = max(sceneDepth, 0.0);

25

tGround = observerAltitude / max(-rayDir.y, 1e-4);

26

rayEnd = min(rayEnd, tGround);

29

float stepSize = (rayEnd - rayStart) / float(PRIMARY_STEPS);
  • این به ما امکان می دهد در نمونه برداری از پرتوهایی که به اجسام مجاور یا زمین برخورد می کنند بسیار دقیق باشیم: stepSize کوچک خواهد بود.

  • We can afford to be a little less precise for rays that travel farther, since they cover greater distances and we distribute an equivalent amount of sample points along them.

The playground below renders the same shader we assembled earlier, but this time as a post-processing effect, allowing us to render atmospheric scattering throughout the scene, taking into account its geometries, with our sky shader as the background.

توجه کنید که چگونه:

  • هر چه اجسام به دوربین نزدیکتر باشند، واضح تر ظاهر می شوند.

  • هر چه اشیاء از دوربین دورتر باشند، محو خواهند شد.

با این پیاده سازی، می توانیم شروع کنیم به ارائه یک آسمان محیط واقعی تر برای هر صحنه ای که به آن نیاز دارد و همچنین با آن کمی سرگرم شویم. تعاملات احمقانه مانند مورد زیر که با a پیاده سازی شده است Raycaster:

اثر جوی پس از درمان

اکنون با اجرام آسمانی کشویی

https://t.co/xejzC5SWuc https://t.co/U342icnvxz

تصویری از سیارات

سرانجام به بخشی می رسیم که احتمالاً در وهله اول به اینجا آمده اید: ارائه اتمسفر واقعی در اطراف سیارات! خوشبختانه، با همه چیزهایی که تا این لحظه ساخته ایم، تنها دو قدم تا رسیدن به این هدف فاصله داریم:

  1. برای رسیدگی به مقیاس های بزرگتر، به یک بافر عمق لگاریتمی بروید.

  2. تعیین کنید که اتمسفر در امتداد یک پرتو مشخص از کجا شروع و متوقف می شود تا شکل آن را مشخص کنید، که می توانید حدس بزنید که یک کره است.

از آنجایی که ما در مقیاس سیاره‌ای در این بخش کار می‌کنیم، وقتی از دور به سیاره خود نگاه می‌کنیم، می‌توانیم انتظار “مبارزه در عمق” زیادی داشته باشیم، زیرا تشخیص عمق بین جو و پوشش سیاره از فاصله دور برای سایه‌بان دشوار است (ارتفاع اتمسفر فقط چند کیلومتر است). ما باید هم نحوه تعریف بافر عمق ما در صحنه React Three Fiber و هم نحوه خواندن آن را تنظیم کنیم. برای این کار تنظیم کردیم logarithmicDepthBuffer به true ج gl حمایت ما Canvas مؤلفه ای که کل تعریف صحنه را در بر می گیرد:

یک بافر عمق لگاریتمی را برای صحنه ما فعال کنید

5

logarithmicDepthBuffer: true,

سپس، در سایه‌زن خود، عمق صحنه خود را به صورت زیر تعریف می‌کنیم تا بافر عمق لگاریتمی به‌دست‌آمده از اثر پس از پردازش را تبدیل کرده و آن را به فاصله در طول پرتو تبدیل کنیم.

تابع getWorldPosition به روز شد

1

float logDepthToViewZ(float depth) {

2

float d = pow(2.0, depth * log2(cameraFar + 1.0)) - 1.0;

6

float logDepthToRayDistance(vec2 uv, float depth) {

7

float viewZ = logDepthToViewZ(depth);

8

vec2 ndc = uv * 2.0 - 1.0;

9

vec4 clipAtZ1 = vec4(ndc, -1.0, 1.0);

10

vec4 viewAtZ1 = projectionMatrixInverse * clipAtZ1;

11

viewAtZ1 /= viewAtZ1.w;

12

vec3 viewRayDir = normalize(viewAtZ1.xyz);

13

float cosTheta = max(-viewRayDir.z, 1e-5);

14

return (-viewZ) / cosTheta;

17

vec3 getWorldPosition(vec2 uv, float depth) {

18

float viewZ = logDepthToViewZ(depth);

19

vec2 ndc = uv * 2.0 - 1.0;

20

vec4 clipAtZ1 = vec4(ndc, -1.0, 1.0);

21

vec4 viewAtZ1 = projectionMatrixInverse * clipAtZ1;

22

viewAtZ1 /= viewAtZ1.w;

23

vec3 viewPos = viewAtZ1.xyz * (viewZ / viewAtZ1.z);

24

vec4 world = viewMatrixInverse * vec4(viewPos, 1.0);

برای نکته دوم استفاده خواهیم کرد تست تقاطع پرتو کره برای تشخیص اینکه خط دید ما از کجا وارد و از کجا خارج می شود کره اتمسفر. هنگامی که این دو نقطه را داشته باشیم، می‌توانیم خط راه‌اندازی مجدد خود را بدون از دست دادن نمونه‌های خارج از جو به این بخش محدود کنیم.

با این حال، انجام تنها یک آزمایش کافی نیست. ما همچنین می‌خواهیم سیاره خود را به‌عنوان یک شبکه کروی که توسط یک کره جوی کمی بزرگ‌تر احاطه شده است مدل‌سازی کنیم، و بنابراین باید همان آزمایش را بر روی خود سیاره انجام دهیم. اگر پرتو قبل از خروج از جو به زمین برخورد کند، از آن تقاطع زمینی به عنوان انتهای بخش راهپیمایی پرتوی خود استفاده می کنیم.

استفاده از تقاطع پرتو-کره در حلقه جستجوی مجدد

1

vec3 planetCenter = vec3(0.0);

3

vec2 atmosphereHit = raySphereIntersect(

10

vec2 planetHit = raySphereIntersect(

18

if (atmosphereHit.x > 0.0 || atmosphereHit.y > 0.0) {

19

float atmosphereNear = max(atmosphereHit.x, 0.0);

20

float atmosphereFar = atmosphereHit.y;

23

if (planetHit.x > 0.0) {

24

atmosphereFar = min(atmosphereFar, planetHit.x);

27

atmosphereFar = min(atmosphereFar, sceneDepth);

31

if (atmosphereFar > atmosphereNear) {

یکی از چیزهای اضافی که ما باید آن را تطبیق دهیم، انتهای بخش ریمارشینگ ما برای رسیدگی به اشیاء در صحنه است. جو می تواند به دو دلیل مختلف متوقف شود:

2

if (planetHit.x > 0.0) {

3

atmosphereFar = min(atmosphereFar, planetHit.x);

6

if (sceneDepth planetHit.x - 2.0) {

7

atmosphereFar = min(atmosphereFar, sceneDepth);

12

atmosphereFar = min(atmosphereFar, sceneDepth);

در هر دو مورد، ما می خواهیم راهپیمایی را در نزدیکترین شی مناسب متوقف کنیم.

قبل از

بعد از

صحنه ما قبل/بعد از در نظر گرفتن عمق صحنه

توجه کنید که سطح سیاره بدون این منطق چگونه خواهد بود در مقابل در سایت ما

با این دو بخش که قبلاً کد شده است، ما یک پیاده‌سازی کامل از پراکندگی جوی به عنوان یک اثر پس پردازش داریم و می‌توانیم اتمسفرهای اطراف سیارات را ارائه کنیم. صحنه زیر یک “سیستم خورشید-زمین” ساده را در React Three Fiber با جلوه ای سفارشی در جای خود به تصویر می کشد. من از شما دعوت می کنم که لحظه ای برای تنظیم موقعیت خورشید، بزرگنمایی و لذت بردن از رنگ های آسمانی که این سایه زن می تواند از زوایای مختلف، از زمین تا مدار، بازتولید کند، صرف کنید.

جلوه‌ای که در این دمو می‌بینید، همان اثری است که برای ساختن عکس‌های پوسترهایی که در اوایل آوریل برای اعلام این مقاله ارسال کردم، استفاده کردم:

برای مقاله آتی و بسیار موضوعی من در مورد پراکندگی جوی برنامه ریزی کنید من احساس الهام گرفتم و پوسترهایی با عکس های رندر واقعی با تکنیک هایی که در آن یاد خواهید گرفت درست کردم 🙂

برای این https://t.co/wSjdQPyoI0 بسیار هیجان زده هستم

مقابله با کسوف

این کمی است جایزه بخشی که می خواهم به این سوال پاسخ دهم: چگونه می توانیم با اجرام بزرگ آسمانی که خورشید را مسدود می کنند مقابله کنیم؟ ما در حال حاضر درک مناسبی از آنچه در این سایه‌زن پراکنده جوی در حال بازی است در مورد نورپردازی داریم، و اضافه کردن این آزمایش اضافی نسبتاً آسان است.

ما می‌توانیم پس از تابع lightMarch، یک تابع فراخوانی اضافه کنیم که باز خواهد گشتsunVisibility اعم از [0, 1] و نفوذپذیری را در این مقدار ضرب کنید. خود تابع می تواند به سادگی انجام یک محصول نقطه ای بین:

اگر خیلی تطابق داشتند، یعنی. نزدیک به 1.0، این بدان معنی است که ماه با خورشید تداخل پیدا می کند و بالعکس. اگر آنها متعامد بودند، نزدیک به 0.0، هیچ مانعی وجود نخواهد داشت. با این حال، این اندازه و مقیاس جسم در صحنه را در نظر نمی گیرد.

نموداری که سناریوهای موانع مختلف را نشان می دهد که توسط تست دید خورشید ما انجام می شود
نموداری که سناریوهای موانع مختلف را نشان می دهد که توسط تست دید خورشید ما انجام می شود

ما به تابعی نیاز داریم که بتواند سه مورد توضیح داده شده در نمودار بالا را مدیریت کند:

  • وقتی ماه جلوی خورشید را نمی گیرد.

  • وقتی هست، اما بزرگتر یا نزدیک به اندازه خورشید از POV دوربین است.

  • وقتی هست، اما در شعاع خورشید از POV دوربین قرار می گیرد.

1

float sunVisibility(vec3 point) {

2

vec3 sunDir = normalize(sunDirection);

3

vec3 toMoon = moonPosition - point;

4

float moonDist = length(toMoon);

5

vec3 moonDir = normalize(toMoon);

12

float angularSep = acos(clamp(dot(sunDir, moonDir), -1.0, 1.0));

13

float sunAngularRadius = SUN_RADIUS / SUN_DISTANCE;

14

float moonAngularRadius = moonRadius / moonDist;

15

float outerEdge = sunAngularRadius + moonAngularRadius;

18

if (dot(sunDir, moonDir) 0.9) {

23

if (moonAngularRadius >= sunAngularRadius) {

24

float innerEdge = moonAngularRadius - sunAngularRadius;

25

return max(0.075, smoothstep(innerEdge, outerEdge, angularSep));

28

float innerEdge = sunAngularRadius - moonAngularRadius;

29

float minVisibility = clamp(

30

1.0 - (moonAngularRadius * moonAngularRadius) / (sunAngularRadius * sunAngularRadius),

36

return mix(minVisibility, 1.0, smoothstep(innerEdge, outerEdge, angularSep));

اینجا float angularSep = acos(clamp(dot(sunDir, moonDir), -1.0, 1.0)) نشان دهنده جدایی زاویه ای بین جهت های خورشید و ماه است.

  • dot(sunDir, moonDir) نشان دهنده تراز بین دو جهت است.

  • acos آن را دوباره به زاویه تبدیل می کند.

سپس می توانیم از این مقدار برای مقایسه آن با آستانه های زاویه ای مختلف استفاده کنیم outerEdge و innerEdge، به ترتیب زوایایی را نشان می دهد که در آن دو دیسک شروع به تماس خارجی / داخلی می کنند.

نسخه ی نمایشی زیر این را پیاده سازی می کند sunVisibility در مثال قبلی ما عمل می کند و همچنین یک شبکه ماه به سیستم ما اضافه می کند. سعی کنید ماه را با خورشید هماهنگ کنید و متوجه شوید که سایه انداز پراکنده جوی ما چگونه به درستی کمبود نور را در این موارد مدیریت می کند.

برای تغییر اسلایدها از کلیدهای جهت دار چپ و راست استفاده کنید.اسلاید 1 در 4.

جو دنیای بیرون

دیگری جایزه بخش این روز شانس شماست! مدلی که در این مقاله برای شبیه‌سازی چگالی و پراکندگی اتمسفر استفاده کردیم، عمدتاً توسط چند ثابت کنترل می‌شود:

  • شعاع سیاره و جو

  • RayleighScaleHeight و RayleighBeta.

  • MieScaleHeight، MieBeta، mieBetaExt، و mieG

  • OzoneHeight و OzoneWidth

اینها دستگیره های اصلی هستند که باعث می شوند اتمسفر رندر شده ما به همان شکلی به نظر برسد. بنابراین، با تنظیم آنها بر روی مجموعه مقادیر مناسب، می توانیم در تئوری جو مریخ یا حتی سیارات دیگر را تقریب بزنیم. در زیر مجموعه مقادیری است که من برای مریخ تعیین کردم:

5

atmosphereRadius: 3500,

6

rayleighScaleHeight: 11.1,

7

rayleighBeta: new THREE.Vector3(0.019, 0.013, 0.0057),

12

ozoneCenterHeight: 0.0,

14

ozoneBetaAbs: new THREE.Vector3(0.0, 0.0, 0.0),

16

planetSurfaceColor: '#8B4513',

فقط جایگزین کردن ثابت هایمان با این ها، فضایی غبارآلودتر و نارنجی به ما می دهد. حتی بهتر از آن، ما رنگ آبی متمایز مریخ را در غروب خورشید دریافت می کنیم! در زیر اسکرین شات هایی که در حین کار بر روی آن گرفته ام آورده شده است. می توانید سعی کنید این مقادیر را به نسخه آزمایشی قبلی متصل کنید تا نتیجه را خودتان ببینید.

برای تغییر اسلایدها از کلیدهای جهت دار چپ و راست استفاده کنید.اسلاید 1 در 3.

پراکندگی اتمسفر بر اساس LUT

سایه‌زن به‌دست‌آمده که ما ایجاد کردیم، اگرچه بصری است و قادر به رندر کردن اتمسفرها در مقیاس‌های کوچک و بزرگ است، اما متاسفانه اجرای آن بسیار گران است:

  • ما مقدار زیادی داریم PRIMARY_STEPS در چرخه راهپیمایی ما

  • ما یک چرخه راهپیمایی نور تو در تو داریم.

  • ما تمام محاسبات ریاضی را با وضوح تمام صفحه انجام می دهیم.

در کنار رفع این کاستی ها می خواستم یاد بگیرم چگونه حرفه ای ها این کار را انجام دادند وقتی در مطالعه ام در مورد پراکندگی جوی به این نقطه رسیدم. سباستین هیلر در مقاله خود با عنوان تکنیک مقیاس پذیر و آماده تولید برای تصویربرداری از آسمان و جو، یک روش تصویربرداری جوی بر اساس جستجو در جداول (LUTs)، یعنی بافت هایی که می توانند محاسبات پراکندگی گران قیمت را پشتیبانی کنند، بنابراین رندر نهایی این بافت های از پیش محاسبه شده را نمونه برداری و ترکیب می کند.

در این قسمت به پیاده سازی های مربوطه می پردازیم:

  • پهنای باند LUT، که مقدار نوری را که از طریق جو زنده می ماند ذخیره می کند.

  • نمایش آسمان LUT، که رنگ آسمان حاصل را برای یک موقعیت دوربین مشخص ذخیره می کند

  • پرسپکتیو هوایی LUT، که مه/مه جوی را بین دوربین و هندسه های صحنه قابل مشاهده، از جمله میزان نور اضافه شده توسط پراکندگی و تأثیر آن بر رنگ های صحنه، ذخیره می کند.

پهنای باند LUT

در سایه زن اصلی ما، هر نقطه نمونه تماس می گیرد lightmarch عملکردی برای دریافت مقدار نوری که از خورشید به آن می رسد را دریافت می کند، که حدس زدید بسیار گران است. هدف این LUT این است که این داده ها را از قبل ذخیره کند، ترجیحاً با وضوح پایین، به طوری که در صورت نیاز به این داده های سبک، بتوانیم آن را در LUT های بعدی بارگذاری کنیم.

اجرای من برای این LUT و هر LUT بعدی شامل موارد زیر است:

  1. یک شیء فریم بافر ویژه با وضوح خاص تعریف کنید. مخصوصا برای این یکی که انتخاب کردم 250 x 64.

  2. متریالی را با سایه زن سفارشی تعریف کنید که از منطق تولید داده های LUT ما پشتیبانی می کند.

  3. آن را در یک صحنه خاص روی یک چهارگوش تمام صفحه اعمال کنید، در این مورد، transmittanceLUTScene.

  4. صحنه را رندر کنید و بافت حاصل را به صورت یکنواخت به پایین دست LUT منتقل کنید.

ممکن است کمی پیچیده به نظر برسد، اما همانطور که قبلاً گفتیم، در حالت ایده‌آل شما از WebGPU و محاسبات سایه‌زنان برای این کار استفاده می‌کنید، و بنابراین به آن FBOها نیاز ندارید.

برای عبور، خط گران قیمت مارش نور را با قرار دادن آن در گذرگاه خود استخراج می کنیم transmittanceLUTFragmentShader. کد زیر همان چیزی است که من برای ایجاد بافت خود استفاده کردم:

2

float mu = mix(-1.0, 1.0, vUv.x);

4

float radius = mix(planetRadius, atmosphereRadius, vUv.y);

5

vec3 rayOrigin = vec3(0.0, radius, 0.0);

6

float sinTheta = sqrt(max(1.0 - mu * mu, 0.0));

7

vec3 rayDir = normalize(vec3(sinTheta, mu, 0.0));

9

vec2 atmosphereHit = raySphereIntersect(

16

vec2 planetHit = raySphereIntersect(

23

float rayLength = atmosphereHit.y;

26

gl_FragColor = vec4(1.0);

30

if (planetHit.x > 0.0) {

31

gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);

35

float stepSize = rayLength / float(TRANSMITTANCE_STEPS);

36

float rayleighOD = 0.0;

40

for (int i = 0; i TRANSMITTANCE_STEPS; i++) {

41

float t = (float(i) + 0.5) * stepSize;

42

vec3 samplePoint = rayOrigin + rayDir * t;

43

rayleighOD += rayleighDensity(samplePoint) * stepSize;

44

mieOD += mieDensity(samplePoint) * stepSize;

45

ozoneOD += ozoneDensity(samplePoint) * stepSize;

49

rayleighBeta * rayleighOD +

51

ozoneBetaAbs * ozoneOD;

53

gl_FragColor = vec4(exp(-tau), 1.0);
  • برای هر پیکسل، ما از آن پرتو می‌کنیم vec3(0.0, radius, 0.0)، که بین رشد می کند planetRadius و atmosphereRadius در امتداد vUv.y هماهنگ کردن

  • جهت rayDir جهت نور را برای هر پیکسل معینی از LUT ما تعیین می کند که بین آنها متفاوت است mu = -1، یعنی جهت پایین به سطح سیاره، rayDir = vec3(0.0, -1.0, 0.0)، و mu = 1 جهت بالا به فضا، rayDir = vec3 (0.0, 1.0, 0.0). چه زمانی mu = 0، rayDir = vec3(1.0, 0.0, 0.0) به این معنی که نور به صورت افقی حرکت می کند و جو را می چراند.

  • ما از همان استفاده می کنیم raySphereIntersect و توابع پراکندگی اتمسفر که قبلاً معرفی شد.

این منجر به بافت انتقال زیر LUT می شود:

در اینجا نحوه تفسیر این بافت آمده است:

  • محور x نشان دهنده زاویه نور است. در سمت چپ ما نوری داریم که مستقیماً به زمین نگاه می کند، از این رو رنگ های تیره است. سمت راست، از سوی دیگر، نور را نشان می دهد که مستقیم به بالا به نظر می رسد.

  • محور y نشان دهنده ارتفاع است. پایین تصویر سطح زمین/دریا است، در حالی که قسمت بالایی مرز جو ما است.

  • هنگامی که نور مسیر روشنی داشته باشد، سفید خالص نشان دهنده 100٪ عبور است.

  • نواحی سیاه/رنگی نمایانگر زمین/قسمتی است که هوا در آن متراکم‌تر است، به‌ویژه در نزدیکی زمین که مقداری از نور ناپدید شده است.

LUT های بعدی اکنون می توانند به این سوال پاسخ دهند “چقدر نور در یک زاویه و ارتفاع معین در جو ما زنده می ماند” خیلی سریع فقط من به بالا نگاه می کنم این مقدار در این بافت

نمای هوایی از آسمان و LUT

این دو LUT از داده های انتقالی که ما در بافت مربوطه آنها محاسبه کردیم استفاده می کنند و به دو سؤال مکمل پاسخ می دهند:

  1. اگر از زمین به سمت بالا نگاه کنم، آسمان چه رنگی است؟ رنگ آسمان

  2. بین موقعیت فعلی من و هر شیء در صحنه چقدر اتمسفر وجود دارد؟ مه جوی

نموداری که فرآیندهای نمونه برداری برای LUT های Sky-View و Aerial Perspective ما را نشان می دهد
نموداری که فرآیندهای نمونه برداری برای LUT های Sky-View و Aerial Perspective ما را نشان می دهد

ترکیب دو LUT به ما اثر پراکندگی جوی کامل را می دهد. اولی رنگ میدان دور را کنترل می کند، در حالی که دومی مه میدان نزدیک را محاسبه می کند. با استفاده از یک فرآیند مشابه شامل FBO ها و صحنه های خارج از صفحه، می توانیم سایه بان های مختلفی را برای تولید هر دو LUT تعریف کنیم.

برای بافت Sky View کد زیر را دریافت کردم:

گزیده ای از Sky View LUT

1

vec3 getSkyViewForward(vec3 up) {

3

vec3 projectedSun = sunDirection - up * dot(sunDirection, up);

4

return normalize(projectedSun);

7

vec3 getSkyViewRayDir(vec2 uv, vec3 up) {

8

vec3 forward = getSkyViewForward(up);

9

vec3 right = normalize(cross(forward, up));

12

float azimuth = (uv.x * 2.0 - 1.0) * PI;

15

float elevation = (uv.y * uv.y - 0.5) * PI;

17

float cosElevation = cos(elevation);

18

vec3 horizontal = cos(azimuth) * forward + sin(azimuth) * right;

20

return normalize(horizontal * cosElevation + up * sin(elevation));

24

vec3 rayOrigin = uCameraPosition;

25

vec3 up = normalize(rayOrigin);

26

vec3 rayDir = getSkyViewRayDir(vUv, up);

27

vec3 planetCenter = vec3(0.0);

29

vec2 atmosphereHit = raySphereIntersect(rayOrigin, rayDir, planetCenter, atmosphereRadius);

30

vec2 planetHit = raySphereIntersect(rayOrigin, rayDir, planetCenter, planetRadius);

33

if (atmosphereHit.y 0.0) {

34

gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);

39

float atmosphereNear = max(atmosphereHit.x, 0.0);

40

float atmosphereFar = atmosphereHit.y;

41

if (planetHit.x > atmosphereNear) {

42

atmosphereFar = min(atmosphereFar, planetHit.x);

45

float atmosphereSegmentLength = atmosphereFar - atmosphereNear;

46

float stepSize = atmosphereSegmentLength / float(SKY_VIEW_STEPS);

53

gl_FragColor = vec4(scatteredLight, 1.0);

نکته اصلی که در اینجا باید تأکید کرد این است getSkyViewRayDir، که جهت پرتوهای پرتوهای ما را تعیین می کند. در این مورد:

  • محور x، vUv.x به تبدیل می شود آزیموت، یعنی جهت های چپ به راست از [-PI, PI].

  • محور y، vUv.y، مطابقت دارد ارتفاع، به عنوان یک تبدیل درجه دوم (vUv.y * vUv.y - 0.5) * PI 3، یعنی زاویه عمودی ما از آسمان در محدوده [-PI/2, PI/2].
  • در نهایت این دو زاویه را به سه بعدی تبدیل می کنیم rayDir: up به آسمان اشاره کرد forward در امتداد افق به سمت خورشید و right به ما این امکان را می دهد که به چپ و راست در اطراف آسمان بکشیم.

با این تعریف ما rayDir، خط مجدد ما در اینجا بافتی را ارائه می دهد که رنگ آسمان را برای جهت ها در سراسر گنبد آسمان نشان می دهد.

در مورد پرسپکتیو هوایی، همانطور که قبلا ذکر شد، من کمی از مقاله هیلر منحرف شدم. بافت به دست آمده من یک بافت دو بعدی است که در آن هر پیکسل با یک پیکسل صفحه قابل مشاهده مطابقت دارد. من به بافر عمق صحنه تکیه می‌کنم تا بگویم چقدر باید به پایین پرتو برویم و پراکندگی را جمع کنیم.

در نتیجه، این به من اجازه می دهد تا از همان کد پراکندگی معرفی شده در قسمت اول استفاده کنم، با این تفاوت که اکنون هر نمونه دید نور خورشید را از Transmittance LUT دریافت می کند. نتیجه، پراکندگی اتمسفر انباشته شده را در RGB و یک مقدار انتقال نمایش بسته شده در آلفا ذخیره می کند، که بعداً در طول ترکیب از آن استفاده خواهیم کرد.

نمونه پرسپکتیو هوایی LUT

2

float depth = texture2D(depthBuffer, vUv).x;

5

vec3 rayOrigin = uCameraPosition;

6

vec3 worldPosition = getWorldPosition(vUv, depth);

7

vec3 rayDir = normalize(worldPosition - rayOrigin);

8

float sceneDepth = logDepthToRayDistance(vUv, depth);

10

vec2 atmosphereHit = raySphereIntersect(rayOrigin, rayDir, vec3(0.0), atmosphereRadius);

11

vec2 planetHit = raySphereIntersect(rayOrigin, rayDir, vec3(0.0), planetRadius);

13

if (atmosphereHit.y 0.0) {

14

gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);

20

float atmosphereNear = max(atmosphereHit.x, 0.0);

21

float atmosphereFar = atmosphereHit.y;

23

if (planetHit.x > 0.0) {

24

atmosphereFar = min(atmosphereFar, planetHit.x);

26

if (sceneDepth planetHit.x - 2.0) {

27

atmosphereFar = min(atmosphereFar, sceneDepth);

30

atmosphereFar = min(atmosphereFar, sceneDepth);

33

float segmentLength = atmosphereFar - atmosphereNear;

34

float stepSize = segmentLength / float(AERIAL_PERSPECTIVE_STEPS);

37

for (int i = 0; i AERIAL_PERSPECTIVE_STEPS; i++) {

38

float t = atmosphereNear + (float(i) + 0.5) * stepSize;

39

vec3 samplePoint = rayOrigin + rayDir * t;

42

vec3 sunTransmittance = sampleTransmittanceLUT(samplePoint, sunDirection);

49

gl_FragColor = vec4(scatteredLight, packedTransmittance);

ترکیب

با نمای آسمان و پرسپکتیوهای هوایی تولید شده، فقط یک مرحله باقی مانده است: ترکیب آنها در یک گذر نهایی پس از پردازش برای رسیدن به نتیجه کامل پراکندگی جوی مبتنی بر LUT. کد عمدتاً شامل موارد زیر است:

  • تبدیل rayDir فعلی به مختصات skyViewUV به طوری که در هر جهتی در آسمان، می دانیم از کجا از LUT از پیش محاسبه شده Sky-view نمونه برداری کنیم.

1

vec2 getSkyViewLUTUv(vec3 rayDir, vec3 planetCenter) {

2

vec3 up = normalize(uCameraPosition - planetCenter);

3

vec3 forward = getSkyViewForward(up);

4

vec3 right = normalize(cross(forward, up));

6

float vertical = clamp(dot(rayDir, up), -1.0, 1.0);

7

vec3 horizontal = rayDir - up * vertical;

11

float azimuth = atan(dot(horizontal, right), dot(horizontal, forward));

12

float elevation = asin(vertical);

13

float elevation01 = clamp(elevation / PI + 0.5, 0.0, 1.0);

16

azimuth / (2.0 * PI) + 0.5,

21

vec3 sampleSkyViewLUT(vec3 rayDir, vec3 planetCenter) {

22

vec2 uv = getSkyViewLUTUv(rayDir, planetCenter);

23

return texture2D(skyViewLUT, uv).rgb;
  • بازسازی پرتو دید از بافر عمق و بررسی اینکه آیا آن پرتو به سیاره برخورد می کند یا خیر.

  • استفاده از یک پرسپکتیو هوایی LUT برای هندسه صحنه، با استفاده از کانال آلفا آن به عنوان انتقال دید و کانال های RGB آن به عنوان نور منتشر.

  • نمونه‌برداری از Sky View LUT برای پیکسل‌های پس‌زمینه.

1

void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {

2

float depth = readDepth(depthBuffer, uv);

3

vec3 rayOrigin = uCameraPosition;

4

vec3 rayDir = normalize(getWorldPosition(uv, depth) - rayOrigin);

5

vec3 planetCenter = vec3(0.0);

6

vec2 planetHit = raySphereIntersect(rayOrigin, rayDir, planetCenter, planetRadius);

7

vec3 color = inputColor.rgb;

9

bool isBackground = depth >= 1.0 - 1e-7;

12

if (aerialPerspectiveEnabled && !isBackground) {

13

vec4 aerialPerspective = sampleAerialPerspectiveLUT(uv);

14

color = color * aerialPerspective.a + aerialPerspective.rgb;

18

if (skyViewEnabled && isBackground) {

19

color = inputColor.rgb + sampleSkyViewLUT(rayDir, planetCenter);

22

color = ACESFilm(color);

23

color = pow(color, vec3(1.0 / 2.2));

25

outputColor = vec4(color, 1.0);

زمین بازی زیر شامل اجرای کامل اتمسفر مبتنی بر LUT ما است: همه LUT ها و سایه زن مربوط به آنها، و همچنین پاس نهایی پس از پردازش. کمی متراکم است، بنابراین توصیه می‌کنم پیاده‌سازی را مستقیماً در این پیوند Github بررسی کنید، جایی که کدی را که صحنه را در زیر ارائه می‌کند، پیدا خواهید کرد.

این نسخه از پراکندگی جوی ممکن است تقریباً مشابه نسخه ای باشد که در قسمت های قبلی این پست روی آن کار کردیم، اما روند اساسی متفاوت است: ما کار را به LUT های کوچکتر تقسیم می کنیم که سپس آنها را در اثر نهایی ترکیب می کنیم. مهمتر از همه، به جای تابش مکرر به خورشید برای اینکه بفهمیم چه مقدار نور به هر نمونه می رسد، می توانیم این اطلاعات روشنایی را مستقیماً از Transmittance LUT استخراج کنیم، حلقه تودرتو پرهزینه را با یک جستجوی بافت ساده جایگزین کنیم و منجر به بهبود عملکرد غیر قابل اغماض برای صحنه نهایی شود.

با این حال، پیاده سازی مبتنی بر LUT من در مقایسه با آنچه سباستین هیلر و دیگران در این زمینه به آن دست یافته اند، کمرنگ است:

  • برخی رگه ها و سوسو زدن ها به خصوص در نمای آسمان رخ می دهد

  • میانبرهایی که من انتخاب کردم، روند را کمتر از آنچه که می‌توانست بهینه کرد.

  • احتمالا باید از همان ابتدا از WebGPU استفاده می کردم.

اگر می‌خواهید به یک پیاده‌سازی در سطح تولید واقعی نگاه کنید، من به شدت توصیه می‌کنم سه فضای مکانی را توسط شدا ماتسودا بررسی کنید.@shotamatsuda). کار او بر روی آسمان ها، ابرها و تصویربرداری فضایی نقطه شروع بزرگی برای من بود و تصاویری که او در رسانه های اجتماعی به اشتراک می گذارد خود گویای این موضوع است.

با این حال یاد گرفتم زیاد در طول این پروژه، به ویژه از طریق رویکرد مبتنی بر LUT، که وقتی نوبت به ایجاد جلوه‌های پس از پردازش متناسب با عمق فضای صفحه می‌رسد، من را از منطقه راحتی خود خارج کرد. همچنین برخی از دانش قبلی را تثبیت کرد و منجر به یک سری تجسم های زیبا شد (که در نهایت مهمترین چیز است).

من از نتیجه این آزمایش ها بسیار راضی هستم. من همچنین روی افزودن ابرهای حجم کار کردم، اما نتیجه هنوز کمی مختلط است و قبل از اینکه بتوانم آنقدر به آن افتخار کنم که آن را در یک بررسی نشان دهم، به کار بیشتری نیاز دارد. که باید صبر کرد. تا آن زمان، من مشتاقانه منتظر استفاده از این اثر برای تکمیل پروژه ها و صحنه های آینده ام هستم کم کم داشتم در سرم شکل می گرفتم.

  1. من این را امتحان کرده ام به طور مکرر

  2. منظره‌های ابری بی‌درنگ با ریمارشینگ حجمی بسیاری از مفاهیم مورد استفاده در اینجا را معرفی می کند.

  3. این یک راه حل برای جلوگیری از پلک زدن بیش از حد در نمای بلند آسمان است




Source link

نوشته های مشابه

دیدگاهتان را بنویسید

همچنین ببینید
بستن
دکمه بازگشت به بالا