درباره Shaping Light: نورپردازی حجمی در زمان واقعی با پس پردازش و Raymarching برای وب

همانطور که در چند ماه گذشته با پس پردازش بیشتر آشنا شدم، کنجکاو بودم که این تکنیک های تازه آموخته شده را فراتر از سبک سازی خالص برای دستیابی به چیزی بیشتر پیش ببرم. کاربردی. میخواستم راههای جدیدی برای غنیسازی کار سهبعدیام پیدا کنم که بدون استفاده از افکتها و شکافها همراه با سایهزنهای سفارشی امکانپذیر نباشد.
همانطور که به نظر می رسد، پس پردازش یک نقطه ورود عالی برای تقویت یک صحنه سه بعدی با جلوه های جوی و نور است، و اجازه می دهد تا تصاویر واقعی تر و دراماتیک تر را ارائه دهید. از آنجایی که این اثرات در فضای صفحه نمایش، هزینه عملکرد آنها از پیچیدگی صحنه اصلی جدا شده است و آنها را به یک راه حل مؤثر برای متعادل کردن عملکرد و کیفیت بصری تبدیل می کند. در پایان، وقتی با افکت کار می کنیم، هنوز هستیم ساده ترسیم پیکسل بر روی صفحه نمایش
از جمله این اثرات، نورپردازی محیط اولین کسی بود که توجه من را به خود جلب کرد زیرا همیشه می خواستم این پرتوهای زیبا از نور را ایجاد کنم که در میان درختان یا ساختمان ها می تابد و حال و هوای رویایی و جوی را در کارم ایجاد کند.
(بهشدت در «Clair Obscur: Expedition 33» که من اخیراً برای آن بازی کردماهداف تحقیق“). علاوه بر آن،
متوجه شدم که این جلوه های نوری را می توان با آن انجام داد تجدید نظر گسترده، موضوعی که در گذشته به آن پرداخته ام، اما تاکنون هیچ کاربردی عملی در کار خود با React Three Fiber پیدا نکرده ام!
بنابراین، با ترکیب این دو بخش به ظاهر نامرتبط، که پس پردازش و بازسازی هستند، در زمینه نورپردازی حجمی، مجموعه جدیدی از ترفندها را کشف کردم تا کارم را با سایه بان ها تکمیل کنم، تصویر بصری صحنه هایم را بهبود بخشم و مهمتر از آن با شما به اشتراک بگذارم. در این مقاله من نه تنها نشان خواهم داد که چه چیزی باعث استفاده از یک جلوه نور حجمی خوب می شود راهپیمایی مجدد، بلکه جزئیات هر تکنیکی که در پشت آن وجود دارد، مانند هماهنگ کردن تحولات فضایی، نقشه برداری سایهو استفاده جدید از بافرهای عمق، و همچنین گسترش بیشتر در نور چند جهته و نمونه های چند نوری.
Raymarching Light با پردازش پست
بهعنوان کسی که تنها تجربهاش با پسپردازش برای اهداف سبکسازی بود، استفاده از آن در کنار افزایش حجم، که من بهطور جداگانه به طور گسترده مطالعه کردم، جذاب بود. ترکیب حاصل اجازه می دهد تا جلوه های نوری تصادفی بکشید بر روی صحنه ای بر اساس موقعیت دوربین، منبع نور، و همه اینها در حالی که اشیاء زیادی را که می توانند صحنه را تشکیل دهند، در نظر بگیرید.
با این حال، هنوز یک شکاف واضح وجود داشت که باعث شد امتیاز در ابتدا دور از دسترس باشد: Raymarching در فضای سه بعدی کار می کند، در حالی که پس پردازش در فضای صفحه نمایش زندگی می کند که دو بعدی است.. بنابراین، قبل از اینکه به هر چیزی مربوط به نور حجمی مناسب بپردازیم، ابتدا باید در مورد فرآیندی که به ما اجازه می دهد از یکی به دیگری پرش کنیم، بیاموزیم: تغییرات سیستم مختصات.
سیستم های مختصات و تحولات
صحنه های سه بعدی در چندین سیستم مختصات کار می کنند که هر کدام نقش خاصی دارند:
-
فضای شی/مدل: که در آن مختصات نسبت به مبدا جسم است.
-
فضای جهانی: سیستم مختصات مشترک بین تمام اشیاء در صحنه.
-
به فضا نگاه کن: جایی که مختصات نسبت به دوربین است. دوربین در (0، 0، 0) است و به طور پیش فرض به سمت پایین محور z نگاه می کند.
-
فضای کلیپ: که در آن مختصات هنوز با دوربین مرتبط هستند، اما برای انجام “کلیپینگ” تغییر شکل داده اند.
برای کمک به تجسم آنها، نمودار زیر هر سیستم مختصاتی را که در بالا تعریف شده است، نشان می دهد.
با توجه به اینکه کار نورپردازی حجمی ما در فضای صفحه شروع می شود، به دلیل اتکای ما به پس پردازش، باید پرتوهای سه بعدی دوربین خود را از طریق هر پیکسل از اثر خود با تبدیل مختصات فضای صفحه به مختصات فضای جهان بازسازی کنیم. با فرمول های زیر می توانیم به این مهم برسیم:
xNDC = uv.x * 2.0 - 1.0
yNDC = uv.y * 2.0 - 1.0
zNDC = depth * 2.0 - 1.0
clipSpace = { x: xNDC, y: yNDC, z: zNDC, 1.0 }
worldSpace = viewMatrixInverse * projectionMatrixInverse * clipSpace
worldSpace /= worldSpace.w
کجا uv مختصات UV قطعه فعلی نورپردازی حجمی ما پس پردازش پاس است و depth، است بافت عمیق صحنه اصلی که در همان UV گرفته شده است.
از آن می توانیم تابع GLSL را که در مثال های زیر استفاده خواهیم کرد استخراج کنیم:
تابع 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;
اولین پرتو نور ما
اکنون که مفهوم سیستم های مختصات و نحوه پرش از فضای صفحه نمایش (پیمایش پس از پردازش) به فضای جهان (فضای سه بعدی) را می دانیم، می توانیم برای ترسیم اولین نور پرتوی خود به کار بپردازیم. برای انجام این کار، می توانیم یک صحنه ساده را با a شروع کنیم VolumetricLighting اثر، که می تواند استدلال های زیر را داشته باشد:
-
cameraFar: حد عمقی که فراتر از آن چیزی قابل مشاهده یا ارائه نخواهد بود. -
projectionMatrixInverse: روشنcamera.projectionMatrixInverseویژگی که ما از آن برای تبدیل مختصات خود از فضای کلیپ به فضای دید استفاده خواهیم کرد. -
viewMatrixInverse، که ما آن را به عنوان تنظیم می کنیمcamera.matrixWorldویژگی، ماتریسی که مختصات را از فضای دید به فضای جهان تبدیل می کند. -
cameraPosition: موقعیت دوربین ما که راهپیمایی مجدد ما از آن شروع می شود. -
lightDirection: یک بردار نرمال شده که جهت نور را نشان می دهد. -
lightPosition: بردار 3 که موقعیت نور ما را نشان می دهد. -
coneAngle، که نشان دهنده باز بودن دیافراگم نورافکن است.
این ویژگیها تمام آن چیزی است که ما برای به تصویر کشیدن یک پرتو حجمی ساده از نور به شکل مخروط، که از یک نقطه معین سرچشمه میگیرد و به جهت دلخواه اشاره میکند، نیاز داریم.
یکی دیگر از ویژگی های مهم صحنه ما که برای انجام این راه اندازی به آن نیاز داریم، این است بافر عمق. ممکن است متوجه شده باشید که در قسمت سیستم مختصات ذکر شده است، اما در مجموعه ویژگی بالا وجود ندارد. دلیلش این است که خوشبختانه ما آن را به صورت رایگان دریافت می کنیم postprocessingاست Effect کلاس با گذراندن EffectAttribute.DEPTH در سازنده افکت:
جلوه نور کلاس حجم
1
class VolumetricLightingEffectImpl extends Effect {4
projectionMatrixInverse = new THREE.Matrix4(),5
viewMatrixInverse = new THREE.Matrix4(),6
cameraPosition = new THREE.Vector3(),7
lightDirection = new THREE.Vector3(),8
lightPosition = new THREE.Vector3(),11
const uniforms = new Map([12
['cameraFar', new THREE.Uniform(cameraFar)],13
['projectionMatrixInverse', new THREE.Uniform(projectionMatrixInverse)],14
['viewMatrixInverse', new THREE.Uniform(viewMatrixInverse)],15
['cameraPosition', new THREE.Uniform(cameraPosition)],16
['lightDirection', new THREE.Uniform(lightDirection)],17
['lightPosition', new THREE.Uniform(lightPosition)],18
['coneAngle', new THREE.Uniform(coneAngle)],21
super('VolumetricLightingEffect', fragmentShader, {22
attributes: EffectAttribute.DEPTH,26
this.uniforms = uniforms;29
update(_renderer, _inputBuffer, _deltaTime) {30
this.uniforms.get('projectionMatrixInverse').value =31
this.projectionMatrixInverse;32
this.uniforms.get('viewMatrixInverse').value = this.viewMatrixInverse;33
this.uniforms.get('cameraPosition').value = this.cameraPosition;34
this.uniforms.get('cameraFar').value = this.cameraFar;35
this.uniforms.get('lightDirection').value = this.lightDirection;36
this.uniforms.get('lightPosition').value = this.lightPosition;37
this.uniforms.get('coneAngle').value = this.coneAngle;
این بافت عمیق را از طریق یک یونیفرم داخلی نشان می دهد depthBuffer در کد سایه زن ما 2
با خارج شدن از راه، میتوانیم با انجام مراحل زیر شروع به مونتاژ نور دوبارهسازی شده خود کنیم:
-
ما از بافر عمق در یک مقدار مشخص نمونه برداری می کنیم
uvدر فضای صفحه نمایش، زیرا این UV ها مختصات اثر ما را نشان می دهند. -
ما موقعیت سه بعدی را در فضای جهان برای آن پیکسل با استفاده از آن بازسازی می کنیم
getWorldPositionتابعی که در قسمت اول تعریف کردیم.
1
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {2
float depth = readDepth(depthBuffer, uv);3
vec3 worldPosition = getWorldPosition(uv, depth);
-
می پرسیم
rayOriginبه موقعیت دوربین ما -
ما جهت این پرتو را به صورت برداری از دوربین به پیکسل فعلی در فضای جهان تعریف می کنیم.
-
ما نیز مال خود را تنظیم کردیم
lightPosition(در فضای جهان) وlightDirectionبردارها
1
vec3 rayOrigin = cameraPosition;2
vec3 rayDir = normalize(worldPosition - rayOrigin);4
vec3 lightPos = lightPosition;5
vec3 lightDir = normalize(lightDirection);
1
float coneAngleRad = radians(coneAngle);2
float halfConeAngleRad = coneAngleRad * 0.5;5
float lightIntensity = 1.0;8
for (int i = 0; i NUM_STEPS; i++) {9
vec3 samplePos = rayOrigin + rayDir * t;15
vec3 toSample = normalize(samplePos - lightPos);16
float cosAngle = dot(toSample, lightDir);18
if (cosAngle cos(halfConeAngleRad)) {23
float distanceToLight = length(samplePos - lightPos);24
float attenuation = exp(-0.05 * distanceToLight);26
fogAmount += attenuation * lightIntensity;
همه چیزهایی که تاکنون توضیح دادهایم در نسخه ی نمایشی زیر وجود دارد، که زمینه را برای نمونههایی فراهم میکند که در ادامه این مقاله خواهیم دید. در آن خواهید یافت:
-
یک صحنه ساده React Three Fiber.
-
مال ما
VolumetricLightingEffectاثر و تعریف آن -
سایهزن قطعهای از اثر ما.
اکنون یک پرتو نور ساده داریم که با استفاده از بازخوانی حجم ساخته شده است، که با استفاده از پردازش پس از آن بر روی یک صحنه از قبل موجود قرار گرفته است. میتوانید موقعیت نور (فضای جهان) را حرکت دهید و مجموعه صحیح پیکسلهای ترسیم شده در فضای صفحه را ببینید.
توقف عمیق
هنگامی که ما دوباره شروع می کنیم، نور خود را بدون هیچ محدودیتی روی صحنه خود رنگ می کنیم. پرتو ادامه دارد راهپیمایی فراتر از چیزی که دوربین/بیننده می تواند ببیند. بنابراین ما با نور یا سایر جلوههای جوی مواجه میشویم، مانند مه که از طریق دیوارها یا اشیاء قابل مشاهده است، که نه تنها واقعگرایی افکت ما را میشکند، بلکه عملکرد را نیز از دست میدهد، زیرا ما در حال انجام فرآیند فشرده مجدد خود فراتر از جایی هستیم که باید در وهله اول متوقف میشد.
با استفاده از بافت عمقی که در فضای صفحه نمونهبرداری کردهایم و بازسازی نقطه در فضای جهان، میتوانیم فاصله بین دوربین خود و نقطه قطعه فعلی در فضای جهان را محاسبه کنیم تا زمانی که نمونهبرداری فراتر از عمق صحنه را شروع میکنیم، دوباره شروع به کار کنیم:

پیاده سازی ترمز مبتنی بر عمق در حلقه راه اندازی مجدد ما
1
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {2
float depth = readDepth(depthBuffer, uv);3
vec3 worldPosition = getWorldPosition(uv, depth);5
float sceneDepth = length(worldPosition - cameraPosition);9
for (int i = 0; i NUM_STEPS; i++) {10
vec3 samplePos = rayOrigin + rayDir * t;12
if (t > sceneDepth || t > cameraFar) {
با افزودن آن به نسخه ی نمایشی قبلی، مشکل دیده شدن منبع نور از طریق کره هنگام حرکت دوربین در صحنه برطرف می شود.
شکل دادن به پرتو نور ما
ما اکنون یک پاس اول پایدار در زیر نورپردازی حجم داریم. در حالی که در اولین مثال از همان ابتدا به آن شکلی دادیم، هنوز هم میخواستم بخش کوچکی از این مقاله را به شما نشان دهم تا به هر طریقی که میخواهید نور محیط خود را شکل دهید.
دوست من @KennyPierman این کار را در پروژه صحنه نمایشی زیستگاه خود به خوبی انجام می دهد، بازسازی سه بعدی دنیای استوانه ای اونیل که در فضا در حال حرکت است.
او در آن نور حجمی/مه و دیگر اثرات جوی را شکل می دهد سیلندر با استفاده از تابع فاصله کاراکتر مربوطه خود برای تناسب با مورد خاص آن، و ما می توانیم در مثال خود همین کار را انجام دهیم:
استفاده از SDF برای شکل دادن به نور پرتو ما
1
float sdCylinder(vec3 p, vec3 axisOrigin, vec3 axisDir, float radius) {2
vec3 p_to_origin = p - axisOrigin;3
float projectionLength = dot(p_to_origin, axisDir);4
vec3 closestPointOnAxis = axisOrigin + axisDir * projectionLength;5
float distanceToAxis = length(p - closestPointOnAxis);6
return distanceToAxis - radius;9
float smoothEdgeWidth = 0.1;11
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {13
for (int i = 0; i NUM_STEPS; i++) {14
vec3 samplePos = rayOrigin + rayDir * t;16
if (t > sceneDepth || t > cameraFar) {20
float sdfVal = sdCylinder(samplePos, lightPos, lightDir, 2.0);21
float shapeFactor = smoothstep(0.0, -smoothEdgeWidth, sdfVal);23
if (shapeFactor 0.1) {
ما می توانیم از هر SDF در جعبه ابزار خود استفاده کنیم 3، همان مواردی را که 2 سال پیش در پست وبلاگ Raymarching خود بررسی کردیم تا نور خود را آنطور که می دانیم شکل دهیم. دمو زیر چندین نمونه از نورهایی به شکل مخروط، کره، استوانه و چنبره را نشان می دهد.
پس از پایهگذاری اثر نور حجمی خود، اکنون میتوانیم به جنبه پیچیدهتر این مقاله بپردازیم: سایه های کامپیوتر. در این قسمت خواهید دید که چگونه:
-
یک بار دیگر از سیستم مختصات استفاده کنید، اما این بار برای بازگشت از فضای جهان به فضای صفحه نمایش.
-
یک نقشه سایه از صحنه ما ایجاد کنید.
-
از هر دو مفهوم برای متوقف کردن رنگ آمیزی نور در زمانی که توسط اشیاء در صحنه مسدود می شود استفاده کنید.
ایجاد یک نقشه سایه از صحنه ما
اجرای فعلی نورپردازی حجمی ما، سایه های ایجاد شده توسط اشیاء قرار گرفته در مسیر نور را در نظر نمی گیرد. به طور واقع بینانه، ما باید رگه ها یا نوارهای تیره ای را ببینیم که در آن نور خاموش می شود. همچنین، مانند توقف مبتنی بر عمق، فقدان پردازش سایه منجر به عملیات بی فایده می شود: ما نور حجیم را در جایی که نباید نور باشد می کشیم.
نمودار زیر آنچه را که اکنون در مقایسه با آنچه باید از نور دقیق محیط انتظار داشته باشیم را نشان می دهد:

برای حل این مشکل ما نیاز به تولید یک نقشه سایه صحنه ما: بافتی که عمق صحنه را نشان می دهد از دیدگاه نور ما. ما می توانیم با استفاده از همان ترفندی که برای سوزاننده استفاده کردیم به این مهم برسیم 4 در ابتدای سال 2024 برای استخراج نرمال های شی ما:
-
ما یک هدف رندر ویژه برای سایه های خود ایجاد می کنیم
-
ما یک اختصاصی ایجاد می کنیم مجازی دوربین و آن را در همان موقعیت نور خود قرار دهید.
-
ما صحنه را با این دوربین رندر می کنیم.
از آنجایی که ما به عمق صحنه خود نیاز داریم، باید از Three.js استفاده کنیم DepthTexture و در depth گزینه تنظیم شده است true هنگامی که ما هدف رندر خود را ایجاد می کنیم. همچنین باید وضوح بافت عمق حاصل را تنظیم کنیم. من به صورت پیش فرض انتخاب کردم 256 x 256; به یاد داشته باشید، هر چه نقشه سایه بزرگتر باشد، چرخه جستجوی مجدد ما شدیدتر خواهد بود.
برای ما دوربین سبک، باید میدان دید او را اصلاح کنیم یا fov بر این اساس نسبت دهید. در مورد ما، ما می توانیم ارزش آن را بر اساس coneAngle یا radius از نور حجمی ما، بسته به اینکه به ترتیب از نور مخروطی یا استوانه ای استفاده می کنید.
راه اندازی دوربین نور و سایه FBO ما
3
const lightCamera = useMemo(() => {4
const cam = new THREE.PerspectiveCamera(90, 1.0, 0.1, 100);9
const shadowFBO = useFBO(shadowMapSize, shadowMapSize, {11
depthTexture: new THREE.DepthTexture(18
const lightPosition = useRef(new THREE.Vector3(4.0, 4.0, -4.0));19
const lightDirection = useRef(20
new THREE.Vector3().copy(lightPosition.current).negate().normalize()24
const { gl, camera, scene, clock } = state;26
lightCamera.position.copy(lightPosition.current);27
lightCamera.updateMatrixWorld();28
lightCamera.updateProjectionMatrix();30
const currentRenderTarget = gl.getRenderTarget();32
gl.setRenderTarget(shadowFBO);33
gl.clear(false, true, false);34
gl.render(scene, lightCamera);36
gl.setRenderTarget(currentRenderTarget);37
gl.render(scene, camera);
با در دست داشتن این دادههای عمقی ارزشمند، میتوانیم از آن در حلقه راهاندازی پرتوهای خود استفاده کنیم و وقتی نور ما مسدود میشود، از ترسیم پیکسلها جلوگیری کنیم.
محاسبه سایه ها و انسداد
در این بخش به پیاده سازی یک تابع می پردازیم calculateShadow که با توجه به یک نقطه مشخص در فضای جهان، برمیگرداند:
برای رسیدن به این نتیجه، ابتدا باید این نقطه را در نظر بگیریم و مختصات آن را با استفاده از ماتریس های طرح ریزی و دید به فضای صفحه تبدیل کنیم. lightCamera ما فقط ایجاد کردیم
نقطه نمونه برداری فعلی را در فضای صفحه تغییر دهید
1
uniform sampler2D shadowMap;2
uniform mat4 lightViewMatrix;3
uniform mat4 lightProjectionMatrix;4
uniform float shadowBias;6
float calculateShadow(vec3 worldPosition) {7
vec4 lightClipPos = lightProjectionMatrix * lightViewMatrix * vec4(worldPosition, 1.0);8
vec3 lightNDC = lightClipPos.xyz / lightClipPos.w;10
vec2 shadowCoord = lightNDC.xy * 0.5 + 0.5;11
float lightDepth = lightNDC.z * 0.5 + 0.5;
در قطعه کد بالا میتوانیم ببینیم که این فرآیند درست برعکس کاری است که برای اولین پرتو نور خود انجام دادیم. اینجا:
با ایجاد این مختصات، ابتدا میتوانیم با برخی موارد افراطی مقابله کنیم: اگر نقطه بیرون باشد lightCamerafrustum، ما نقطه را به عنوان در نظر بگیرید روشن شده، صرف نظر از اینکه خارج از محدوده مختصات UV معتبر قرار می گیرد یا خارج از صفحه دور دوربین قرار دارد.
تشخیص انسداد – Edge case
1
float calculateShadow(vec3 worldPosition) {2
vec4 lightClipPos = lightProjectionMatrix * lightViewMatrix * vec4(worldPosition, 1.0);3
vec3 lightNDC = lightClipPos.xyz / lightClipPos.w;5
vec2 shadowCoord = lightNDC.xy * 0.5 + 0.5;6
float lightDepth = lightNDC.z * 0.5 + 0.5;10
shadowCoord.x > 1.0 ||12
shadowCoord.y > 1.0 ||
اکنون می توانیم روی هسته منطق سایه تمرکز کنیم. ابتدا باید از آن نمونه برداری کنیم shadowMap با استفاده از مختصات UV تعریف شده توسط shadowCoord. رنگ حاصل نشان دهنده عمق رنگ است نزدیکترین سطح قابل مشاهده از نقطه نظر lightCamera از آن پیکسل داده شده با توجه به اینکه این یک بافت خاکستری است، میتوانیم به یک کانال رنگ نگاه کنیم و آن را با آن مقایسه کنیم lightDepth:
-
اگر
lightDepthجایی که یک پیکسل معین بزرگتر ازshadowMapDepth، نقطه در سایه است. -
در غیر این صورت معنا در سایه نیست.
1
float calculateShadow(vec3 worldPosition) {2
vec4 lightClipPos = lightProjectionMatrix * lightViewMatrix * vec4(worldPosition, 1.0);3
vec3 lightNDC = lightClipPos.xyz / lightClipPos.w;5
vec2 shadowCoord = lightNDC.xy * 0.5 + 0.5;6
float lightDepth = lightNDC.z * 0.5 + 0.5;10
shadowCoord.x > 1.0 ||12
shadowCoord.y > 1.0 ||18
float shadowMapDepth = texture2D(shadowMap, shadowCoord).x;20
if (lightDepth > shadowMapDepth + shadowBias) {
نمودار زیر جنبه های خاص و موارد نهایی راه اندازی فعلی ما را نشان می دهد.

شما می توانید پیش نمایش آنچه ما هستند lightCamera با خروجی دادن نتیجه این تابع و برگرداندن آن به عنوان رنگ نهایی افکت ما می بیند:
1
float shadow = calculateShadow(worldPosition);3
outputColor = vec4(vec3(shadow), 1.0);
پس از تعریف، می توانیم از این تابع به عنوان استفاده کنیم شرط پرش در چرخه راهپیمایی ما چرا الف از دست دادن? زیرا نقاط خارج از نقطه سایه استخراج شده فعلی ممکن است مسدود نشوند. به این ترتیب پرتوهای خود را ادامه میدهیم و نمونهبرداری میکنیم در صورتی که بعداً نیاز به انباشت نور بیشتری داشته باشیم.

در نظر گرفتن سایهها هنگام راهاندازی مجدد
2
for (int i = 0; i NUM_STEPS; i++) {3
vec3 samplePos = rayOrigin + rayDir * t;5
if (t > sceneDepth || t > cameraFar) {9
float shadowFactor = calculateShadow(samplePos);10
if (shadowFactor == 0.0) {
پس از ادغام در نسخه ی نمایشی اصلی خود، می توانیم موارد زیبایی را مشاهده کنیم پرتوهای سایه در نتیجه نور فله ای که توسط کره ما مسدود می شود.
پراکندگی نور و سایر پیشرفت ها
ما اکنون تمام بلوک های ساختمانی برای یک جلوه نور حجمی زیبا داریم: ایجاد نور شکل از طریق حجم با سایههای پرتویی که هنگام مسدود شدن ظاهر میشوند. زمان آن رسیده است که جزئیات و پیشرفتهای کوچکی را به سایهزن پس از پردازش خود اضافه کنیم که نه تنها نور ما را واقعیتر، بلکه عملکرد بیشتری نیز میکند.
عملکرد فاز و نویز
در حال حاضر، نور ما تابع تضعیف زیر است:
float attenuation = exp(-0.05 * distanceTofLight);. در حالی که این تضعیف مبتنی بر فاصله ما را شروع کرد، راههای بهتری برای شبیهسازی انتشار نور در یک حجم وجود دارد.
ابتدا می توانیم شما را معرفی کنیم پراکندگی هدایت شده با استفاده از تابع Henyey-Greenstein. زمانی که سعی میکردم نحوه انباشته شدن نور در ابرهای پرتو پرتو را بهبود بخشم، این ویژگی را بهطور طولانی در منظرههای ابری رویایی زمان واقعی با Raymarching حجمی پوشش دادم. تابع در اینجا همان هدف را دارد. این به ما کمک می کند تا نور واقعی تری را در طول حجم ارائه کنیم:
تابع فاز Henyey-Greenstein
1
float HGPhase(float mu) {2
float g = SCATTERING_ANISO;5
float denom = 1.0 + gg - 2.0 * g * mu;6
denom = max(denom, 0.0001);9
float scatter = (1.0 - gg) / pow(denom, 1.5);
ما میتوانیم نتیجه تابع فاز را هنگام محاسبه سهم روشنایی/درخشندگی گام فعلی در حلقه راهاندازی مجدد خود لحاظ کنیم.
3
for (int i = 0; i NUM_STEPS; i++) {4
vec3 samplePos = rayOrigin + rayDir * t;6
if (t > sceneDepth || t > cameraFar) {11
float shadowFactor = calculateShadow(samplePos);12
if (shadowFactor == 0.0) {18
float sdfVal = sdCone(samplePos, lightPos, lightDir, halfConeAngleRad);19
float shapeFactor = smoothstep(0.0, -smoothEdgeWidth, sdfVal);21
if (shapeFactor 0.1) {26
float distanceToLight = length(samplePos - lightPos);27
vec3 sampleLightDir = normalize(samplePos - lightPos);29
float attenuation = exp(-0.3 * distanceToLight);30
float scatterPhase = HGPhase(dot(rayDir, -sampleLightDir));31
vec3 luminance = lightColor * LIGHT_INTENSITY * attenuation * scatterPhase;
مرحله بعدی نیشگون گرفتن است چقدر نور از طریق:
-
stepDensity: متغیری که شبیه سازی می کند مقدار مه -
stepTransmittance: متغیری که نشان دهنده مقدار نور جذب شده توسط محیط/مه با استفاده از قانون Beers است.
2
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {5
float transmittance = 5.0;6
vec3 accumulatedLight = vec3(0.0);8
for (int i = 0; i NUM_STEPS; i++) {11
float distanceToLight = length(samplePos - lightPos);12
vec3 sampleLightDir = normalize(samplePos - lightPos);14
float attenuation = exp(-0.3 * distanceToLight);15
float scatterPhase = HGPhase(dot(rayDir, -sampleLightDir));16
vec3 luminance = lightColor * LIGHT_INTENSITY * attenuation * scatterPhase;18
float stepDensity = FOG_DENSITY * shapeFactor;19
stepDensity = max(stepDensity, 0.0);21
float stepTransmittance = BeersLaw(stepDensity * STEP_SIZE, 1.0);22
transmittance *= stepTransmittance;23
accumulatedLight += luminance * transmittance * stepDensity * STEP_SIZE;28
vec3 finalColor = inputColor.rgb + accumulatedLight;30
outputColor = vec4(finalColor, 1.0);
با این کار، اکنون نور واقعی تری داریم که از طریق محیطی با چگالی ثابت منتشر می شود. نسخه ی نمایشی زیر کدی را که به تازگی به آن نگاه کردیم در صحنه اصلی خود ترکیب می کند:
البته برای آوردن بیشتر غمگین در نتیجه، میتوانیم محیطی پویاتر و ارگانیکتر را برای انتشار نورمان انتخاب کنیم، مانند غباری متراکمتر از ابرهای نرم. برای این کار می توانیم مقداری اضافه کنیم حرکت براونی فراکتال، که من در پستهای بازخوانی و حجم مجدد معرفی کردم، که به ما امکان میدهد الگوهای نویز پیچیدهای را ارائه دهیم که تغییرات چگالی را که میتوانیم در این جلوههای جوی پیدا کنیم، تقلید میکند.
اضافه کردن صدای مه مانند به اثر ما
3
const float NOISE_FREQUENCY = 0.5;4
const float NOISE_AMPLITUDE = 10.0;5
const int NOISE_OCTAVES = 3;8
vec3 q = p + time * 0.5 * vec3(1.0, -0.2, -1.0);12
float scale = NOISE_FREQUENCY;13
float factor = NOISE_AMPLITUDE;15
for (int i = 0; i NOISE_OCTAVES; i++) {16
f += scale * noise(q);25
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {27
for (int i = 0; i NUM_STEPS; i++) {31
float sdfVal = sdCone(samplePos, lightPos, lightDir, halfConeAngleRad);32
float shapeFactor = -sdfVal + fbm(samplePos);34
if (shapeFactor 0.1) {
نسخه ی نمایشی زیر این افزودنی کوچک اما ضروری را به جلوه نور حجمی ما ادغام می کند و تصور یک پرتو قدرتمند نور را در مه غلیظ ایجاد می کند.
بهبود عملکرد
مانند بسیاری از آزمایشهای ریمارچینگ من، مشکلات اصلی عملکرد اغلب از دانهبندی حلقه ریمارچ، به طور خاص ناشی میشود. اندازه هر مرحله. در راهاندازی فعلی ما، هر مرحله از حلقه چندین فرآیند سنگین مانند نمونهبرداری از بافتها را آغاز میکند تا زمانی که به حداکثر تعداد مراحل برسیم. برای کاهش این موضوع میتوانیم:
-
حداکثر تعداد مراحل را کاهش دهید، اما این باعث می شود که نور آزمایش خیلی زود متوقف شود و در نهایت با عمق کمتری در نور حجمی ما پایان یابد.
-
اندازه گام را افزایش دهید تا سریعتر به حداکثر تعداد گام ها برسید، اما این باعث دیده شدن می شود نوارها و کیفیت خروجی کاهش می یابد.

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

همچنین، با تغییر جزئی الگوی نویز در هر فریم، میتوانیم کیفیت خروجی خود را افزایش دهیم و الگوی پارگی صدا را تقریبا نامحسوس کنیم.
اصطکاک نویز آبی را اضافه کنید
2
uniform sampler2D blueNoiseTexture;6
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {8
float blueNoise = texture2D(blueNoiseTexture, gl_FragCoord.xy / 1024.0).r;9
float offset = fract(blueNoise + float(frame%32) / sqrt(0.5));10
float t = STEP_SIZE * offset;12
for (int i = 0; i NUM_STEPS; i++) {13
vec3 samplePos = rayOrigin + rayDir * t;
دمو زیر این روش را پیاده سازی می کند. در آن من:
این از 5000 تکرار به 100 با کیفیت خروجی مشابه می رسد.
کار خود را با نور بالا ببرید
اکنون که تمام جزئیات افکت نورپردازی پس از پردازش حجمی خود را پیاده سازی کرده ایم، اکنون می توانیم روی چند کاربرد این افکت تمرکز کنیم و مشاهده کنیم که چگونه می تواند ظاهر و احساس یک صحنه را به طور قابل توجهی تغییر دهد. ممکن است این اثر را در بسیاری از بازی های ویدیویی یا آثار هنری دیجیتال به عنوان راهی برای این کار دیده باشید صحنه را با نور محیط پر کنید، هوا را با پرتوهای نور و سایه پر می کند، بنابراین نور را به جای اینکه فقط به سطوح برخورد کند، بیشتر به عنوان یک دارایی تبدیل می شود.
در این بخش میخواستم خلاصهای از برخی از خلاقیتهایم را ارائه دهم که از نورپردازی حجمی در جایی که واقعاً میدرخشد (بدون جناس) استفاده میکنند تا همین کار را انجام دهند. جو اتمسفر روشن روی کار من
قوس ها
این صحنه رو تو ذهنم داشتم مثل برهنه لحظه ای که شروع کردم به بررسی این موضوع. من این سری بلند اما باریک از درهای قوسی را تجسم کردم که پرتوهای نور می توانستند از آن نفوذ کنند. ستونهای بین هر در، سایههای خیرهکنندهای را در برابر پسزمینه روشن نور سمت دیگر ایجاد میکنند.

خوشبختانه، اعمال اثر به من این امکان را داد که این چشم انداز را درک کنم:
-
جنبه تابشی حجمی به ما این امکان را می دهد که به نور خود شکلی را بدهیم که باید در هوا قابل مشاهده باشد، زیرا از در می تابد.
-
نقشه سایه صحنه اطمینان حاصل می کند که پرتوهای نور در جایی که روی صحنه لازم است مسدود می شوند، به خصوص زمانی که به ستون هایی برخورد می کنند که قوس های مختلف را از هم جدا می کنند.
-
پشتیبانی از مه/ابر میتواند ظاهر و احساسی عرفانی به صحنه بدهد و در عین حال نور را برجسته کند.
ایده اضافه کردن پله ها به صحنه را از پروژه مشابه Spline توسط Vlad Ponomarenko دریافت کردم. اینها ساده هستند CylinderGeometry با کاهش شعاع روی هم چیده شده اند. من حتی به خود زحمت ندادم که این نمونه ها را بسازم زیرا فقط چند مرحله است (شاید باید می کردم).
نسخه ی نمایشی زیر تمام جزئیات کوچکی دارد که صحنه را زیبا و در عین حال تاریک می کند. از همان جلوهای که در این مقاله ایجاد کردیم استفاده میکند و راهی عالی برای دیدن تفاوتی است که نور محیط میتواند ایجاد کند.
کمربند سیارکی
من عاشق یک صحنه سه بعدی با مضمون فضایی خوب هستم و همیشه به یک بهانه خوب برای کار روی آن نیاز دارم. در اینجا طبیعی بود که بررسی کنیم که چگونه نور حجمی می تواند روشنایی خورشید یا تاریکی سیاراتی را که بر ستاره خود سایه می زنند، افزایش دهد.
اصل پشت این صحنه نسبتاً ساده است:
-
ما یک منبع نور مرکزی داریم: ستاره ای که a
sphereGeometryبا استفاده از ameshBasicMaterial. من همچنین در همان مختصات a قرار گرفتمpointLightبنابراین ما نور را در هر جهت پخش خواهیم کرد و سایه خواهیم داشت.
2
pointLight castShadow intensity={500.5} />3
sphereGeometry args={[0.5, 32]} />4
meshBasicMaterial color={new THREE.Color('white').multiplyScalar(10)} />
-
دو حلقه سیارکی که هر کدام از مجموعه خود تشکیل شده است
instancedMeshبا استفاده از هندسه سیارکی و سیاهmeshStandardMaterial. این به من اجازه داد تا تعداد اشیاء را در صحنه بدون تأثیر بر عملکرد افزایش دهم. -
جلوه نور حجم ما با رفتار کمی تغییر یافته: جهت نور همیشه از موقعیت اصلی نور به سمت ما، بیننده (موقعیت دوربین) خواهد بود. این به من اجازه داد که فقط از یک پرتو نور در جاهایی که مرتبط/مشاهده است استفاده کنم تاثیر گذاشتن به بیننده که از هر جهت می درخشد.
-
علاوه بر این، دوربین لایت دارای زاویه دید بزرگ 90 درجه خواهد بود و همیشه به دوربین اصلی اشاره خواهد کرد. به این ترتیب، صرف نظر از نقطه نظر خود، وسیع ترین نقشه سایه ممکن را از صحنه خود خواهیم داشت و دسته های سایه هایی را از نور انبوه می بینیم که توسط سیارک های زیادی در صحنه ما پنهان شده است.
1
const lightDirection = new THREE.Vector3()2
.subVectors(camera.position, lightPosition)5
lightCamera.position.copy(lightPosition);6
lightCamera.lookAt(camera.position);7
lightCamera.updateMatrixWorld();8
lightCamera.updateProjectionMatrix();
این به همراه یک نقشه سایه با وضوح بالا منجر به یک صحنه فضایی زیبا می شود و از نور حجمی استفاده می کند تا نور ساطع شده از ستاره را درخشان تر و قدرتمندتر کند.
ممکن است متوجه شوید که پرتوهای سایه در ابتدا سوسو می زنند. این یکی از عوارض جانبی وضوح نقشه سایه است. وضوح بالاتری را امتحان کنید و شاهد پایدارتر شدن اثر باشید. این نوع از مصنوع تمایل دارد در هر صحنه ای رخ دهد که شامل تعداد زیادی اشیاء سایه روشن است، حتی با هندسه های ساده. من هنگام تلاش برای بازتولید کار فوق العاده با مشکل مشابهی روبرو شدم @5tr4n0 هنرمند دیجیتال با استفاده از نور حجمی زیبا.
وسوسه شدم این را در webgl با استفاده از shader work برای نور حجمی بازسازی کنم
نه به خوبی اما هنوز هم سرگرم کننده برای ساخت
https://t.co/c0Q8rL0GA0 https://t.co/PmkI0IKvku
به نوبه خود، من تمایل دارم به یک نقشه سایه 512×512 پیکسل بسنده کنم تا تعادل مناسبی بین عملکرد و حداقل ایجاد سایه ایجاد کنم.
با اجرای کامل افکت ما و کار کردن در چند صحنه نمایشی که اخیراً بررسی کردیم، ممکن است از خود بپرسید: بعدش چیه.
سایهزن نوری حجمی پس از پردازش که در این مقاله ساختهایم، ممکن است بسیار صیقلی به نظر برسد، اما هنوز چند مشکل وجود دارد که در حال حاضر به آنها توجه نمیکنیم:
-
در طول این مقاله، ما فقط در نظر می گیریم یک منبع نور واحد، این افکت در حال حاضر راهی برای مدیریت چندین منبع ندارد.
-
سایه ها را همیشه می توان فقط در جهت نور مشاهده کرد. این در نسخه ی نمایشی سیارک و اینکه چرا من آن را انتخاب کردم بسیار مشهود است
lightCameraهمیشه برای پنهان کردن این محدودیت به دوربین اشاره کنید. اما اگر نور نقطه ای حجیمی را بخواهیم که بتواند سایه ایجاد کند چه می شود جهت های متعدد?
هدف این بخش پایانی پاسخگویی به این سؤالات و ارائه چند مثال است که برای کشف راه حل ها اجرا کردم. همچنین به عنوان نتیجه ای برای کاوش ما در مورد نور حجمی عمل می کند.
برای پاسخ به سوال اول، مقیاس بندی به منابع نوری متعدد، به غیر از نیاز، چندان دشوار نیست تعداد زیادی کد اضافی برای هر منبع نور جدید:
-
جدید
lighCamera -
جدید
shadowFBO -
ارسال هر بافت، ماتریس، و آرگومان های دیگر به اثر
-
محاسبه سایه ها، پراکندگی نور و تجمع نور
پس از انجام، تنها کاری که باید انجام دهید این است که بله ترکیب انباشت نور در هر چراغ و ما روشنایی حجمی خود را با چندین منبع نور کار داریم. نسخه ی نمایشی زیر یک نمونه کار کامل با دو چراغ را نشان می دهد که خواهید دید بسیار طولانی تر از نمونه هایی است که تاکنون دیده ایم:
متأسفانه، راه حل من برای تله دوم نیاز به کمی کار مجدد داشت و نتیجه در بهترین حالت یک هک جالب است.
ما می دانیم که فرآیند پشت پرتوهای سایه ما به الف متکی است lightCamera اشاره در جهت معین میدان دید آن محدود است و بنابراین ما نمی توانیم عکس برداری کنیم shadowMap در کل صحنه برای انجام این کار به یک دوربین در هر جهت نیاز داریم: بالا، پایین، چپ، راست، جلو، عقب و بسیاری از FBO های خاص. این مقدار زیادی کد است. خیلی زیاد. خوشبختانه Three.js یک ابزار مفید به نام a دارد CubeCamera که شش دوربین و یک WebGLCubeRenderTarget برای ذخیره و خواندن بافت حاصل.

راه اندازی CubeCamera و WebGLCubeRenderTarget
1
const shadowCubeRenderTarget = useMemo(() => {2
const rt = new THREE.WebGLCubeRenderTarget(SHADOW_MAP_SIZE, {3
format: THREE.RGBAFormat,5
generateMipmaps: false,6
minFilter: THREE.LinearFilter,7
magFilter: THREE.LinearFilter,13
const shadowCubeCamera = useMemo(() => {14
const cam = new THREE.CubeCamera(17
shadowCubeRenderTarget20
}, [shadowCubeRenderTarget]);
با این حال، مشکلی وجود دارد که وقتی با آن برخورد کردم تقریباً غیرفعال شد: وجود ندارد CubeDepthTexture. بنابراین مجبور شدم به ساختن «بافت عمقی» خودم با این روشها بسنده کنم:
-
کاربرد عرف
shadowMaterialکه فاصله نرمال شده بین تمام اشیاء در صحنه و موقعیت نور را یکنواخت برمی گرداند. -
مواد تمام اشیاء در صحنه را با
shadowMaterial. -
با دوربین مکعبی از صحنه عکس بگیرید.
-
صحنه را به حالت اولیه بازگردانید.
استفاده از shadowMaterial برای استخراج داده های عمق
1
const shadowMaterial = useMemo(3
new THREE.ShaderMaterial({5
varying vec3 vWorldPosition;7
vec4 worldPosition = modelMatrix * vec4(position, 1.0);8
vWorldPosition = worldPosition.xyz;9
gl_Position = projectionMatrix * viewMatrix * worldPosition;13
uniform vec3 lightPosition;14
uniform float shadowFar;15
varying vec3 vWorldPosition;19
float distance = length(vWorldPosition);21
float normalizedDistance = clamp(distance / shadowFar, 0.0, 1.0);23
gl_FragColor = vec4(normalizedDistance, 0.0, 0.0, 1.0);26
side: THREE.DoubleSide,28
lightPosition: { value: new THREE.Vector3() },29
shadowFar: { value: CUBE_CAMERA_FAR },
پس از آن، فقط باید آنچه را که دریافت کرده ایم، منتقل کنیم shadowMapCube برای افکت و شیدر قطعه اصلی ما و اصلاح ما calculateShadow عملکردی برای رسیدگی به این بافت مکعبی جدید. در مرحله بعد، ما یک افکت نورپردازی حجمی داریم که می تواند اشیاء را در سراسر صحنه در هر جهتی در نظر بگیرد و بر این اساس سایه ایجاد کند. نمایش بعدی و پایانی این مقاله حاوی نظر من در این مورد است. باز هم، این را بیشتر یک راهحل هک در نظر بگیرید تا یک راهحل تولید، زیرا نتیجهای که تولید میکند به خوبی چیزی نیست که در نمونههای دیگر جمعآوری کنیم.
امیدوارم شما هم به اندازه من از چگونگی این موضوع و صحنه های نمایشی ساخته شده با استفاده از مجموعه تکنیک های جدید راضی باشید. حس روشنایی حجمی وجود داشت پسوند کامل به کار پس از پردازش من در حالی که برخی از آزمایشهای مجدد قبلی را گسترش میدادم، و به همین دلیل طبیعی بود که در این زمان در مورد آن صحبت کنم.
استفاده از پس پردازش به عنوان ورودی برای کارهای سه بعدی دقیق تر از نظر فیزیکی، علاوه بر سبک سازی در کارهای آینده، تلاش جالبی خواهد بود.
من همچنین مشتاق فرو رفتن در مفاهیم مجاور مانند روشنایی جهانی یا انسداد محیطی، که هنوز برای من مبهم هستند، در حالی که از برخی از ابزارهای جدیدی که در اینجا یاد گرفتیم مانند استفاده می کنم نقشه برداری سایه یا مجازی / دوربین های عکاسی برای اهداف دیگر اما به لطف چیزهایی که در حین نوشتن این مقاله آموخته ام، مطمئن هستم که می توانم به زودی با مقاله ای در مورد این موضوعات و کارهای بلندپروازانه سه بعدی انجام شده در مرورگر شما به زودی با شما تماس بگیرم تا با شما به اشتراک بگذارم.
تا آن زمان، امیدوارم این مقاله کافی باشد تا شما را مشغول نگه دارد و الهام بخش شما باشد تا با نورپردازی حجمی در انتهای خود بیشتر آزمایش کنید!
-
می توانید درباره تقسیم چشم انداز بیشتر بدانید اینجا هم توصیه میکنم چک کنید این ویدیو توسط براندون بریسفورد، که نحوه استخراج ماتریس طرح ریزی پرسپکتیو را به اشتراک می گذارد و جزئیات این مرحله تقسیم چشم انداز را توضیح می دهد.
-
همچنین وجود دارد
CONVOLUTIONویژگی اثر که ok استDEPTHبه شما عملیات ویژه خارج از جعبه برای سایهزن قطعه افکت شما میدهد. تبدیل به این مستندات برای مثال و اطلاعات بیشتر -
حتما باید Inigo Quilez را بررسی کنید فرهنگ لغت SDF و سعی کنید از شکل دادن به نور پرتوهای خود با چند مورد عجیب و غریب لذت ببرید، حتی اگر واقع بینانه نباشد.
-
در مورد کاستیک ما مجبور شدیم مواد مش را با a جایگزین کنیم
NormalMaterialبرای استخراج داده های عادی شی و در دسترس قرار دادن آن به عنوان یک بافت.




