老實說我也不知道該叫啥了這標題wwww
Aura是一個Unity的開源插件,可以實現較為出色的大氣效果(如:體積光,體積霧等等):
傳送門:
Asset store: https://assetstore.unity.com/packages/vfx/shaders/aura-volumetric-lighting-111664
Github: https://github.com/raphael-ernaelsten/Aura
大概的效果是這個樣子的www:


(都是官方的圖x)
那么www它到底是怎么實現的呢www?
(本來以為自己肯定什么都看不懂打算這個假期好好研究一下的x 結果在回來的飛機上就稍微弄明白了一點ww(雖然實現確實很直接簡單www)果然還是比以前厲害了一點點的x(逃
在Aura的github頁面(上面有鏈接)中提到了這樣一張圖,也就是實現的流程圖:

首先第一部分,對應着Aura里的各種光源(平行(Directional)光、點光源等等)和各種Volume(一塊霧)。(Aura這里光源不需要打在霧上(進行散射)從而產生體積光的效果,單一個光源就可以擁有體積光的效果w)
它們只是各種形式的數據結構(這塊volume的形狀,光源的參數等等)保存在內存中,等待隨后的操作將它們打包發送到Compute Shader進行計算得到最終的光照結果。
簡單概括一下的話,整個流程大概是:計算各點顏色,累加,應用到渲染結果上。
主要的光照計算過程發生在 Aura::Frustum::ComputeData() (Aura/Classes/Frustum.cs : 147)中。ComputeData() 函數在Aura的主類(Aura.cs)的 UpdateFrustrum()(更新視錐體)函數(Aura.cs : 351)中被調用,而 UpdateFrustrum() 函數又在同一類下的 OnRenderImage(RT, RT) 函數(Aura.cs : 174)中被調用。OnRenderImage 函數完成了繪制圖像的操作,兩個參數(都是RenderTexture - 兩張紋理,就是在屏幕上要顯示的當前幀)src和dest分別是這個函數的輸入和輸出。在函數 OnRenderImage() 中,用PostProcess的方式(使用一個PixelShader(Aura/Shaders/Shaders/PostProcessShader.shader),后述)把 ComputeShader 計算的最終光照結果(一個3D紋理,對應圖上最下面的Integrated Volumetric Lighting,是 UpdateFrustrum() 的產物,后述)應用到 Unity 按照常規方法渲染出的圖片上面,得到最終的結果。

↑ 輸入圖像(常規渲染結果)

↑ ComputeShader 計算得到的結果(由於是3D紋理所以用了個gif,由近及遠)

↑ 通過 PostProcessShader 把大氣效果應用到渲染結果上,得到最終的圖像。
(1)光照(大氣效果)的計算:
首先,按照設置中的精細程度與渲染范圍(在攝像機的Aura組件下有名為Resolution和Range的設置項),把當前 View Space 的視錐體(也就是攝像機眼前的這個錐體)按照精細度分成很多小塊進行計算。

↑ 就這個椎體,就是Frustrum。
在把視錐體切成許多小塊后,每一個小塊就對應着最終3D紋理中的一個體素(是像素概念從2D到3D的延申),同時每個小塊之間在這一階段上沒有計算,這個過程是高度並行化的(每個thread計算一個體素),交給了ComputeShader。每一個體素的計算過程在ComputeDataComputeShader.compute 中。
在這之前,還對當前的深度緩沖區做了一些處理( ComputeMaximumDepthComputeShader.compute ),得到了視錐體中的深度信息(在視錐體格子的某一條x,y軸上,攝像機最遠能看到哪里):

(計算得到的深度圖,主要是對原圖的信息進行整合,得到在視錐體的3D紋理清晰度下合適的深度圖像)
根據這樣一張深度圖,剔除掉一些沒有作用的體素。(被場景物體遮擋住了)
回過來看光照的計算:
拋開前六萬五千行(...)的宏定義(根據用戶的設置不同(使用 / 忽略光源等)最多可以產生32768種設置組合,所以定義了這么多宏定義)不管,可以看到接下來的計算過程非常的直接:
(這里很多東西都不是很正確,只能當個大概理解一下ww(這里太菜還沒搞懂orz)
1. 首先獲得當前體素對應的世界空間坐標。在這里,這個位置是加了一些Jitter(擾動 / 噪聲)的,讓最終產生的結果更加的柔和(用噪聲彌補清晰度上的不足,之后也有很多地方有這種處理)。
2. 計算每個 Volume 對該點的貢獻。(帶有 "密度" (相當於alpha) 的顏色)
3. 計算每個光源對該點顏色的貢獻。首先通過該光源的 ShadowMap 計算自己是否在它的陰影中,如果在陰影中那么這個光源相當於不存在(+0);如果不在陰影中則被光源照亮,當前點的顏色加上光源的顏色(還有相應的衰減)。同時,如果有 Light Cookie 之類的東西也可以在這里計算出來。
4. 對這個點最終得到的顏色進行一些小處理,如非負性等;
5. 利用前一幀計算的結果和當前幀的結果進行混合,讓最終得到的結果更加的柔和。這一步很關鍵,如果不重復利用前一幀的計算結果,最后渲染出的圖像上可以看出非常明顯的 artifact 。但在混合后,渲染質量有了很大提升。Aura給的默認值是前一幀占90%,當前幀的結果占10%,在60FPS下這是一個較為理想的參數。(怎么momentum都是0.9(x))在混合的過程中,由於攝像機位置的變化,導致前一幀的視錐體與當前幀的視錐體之間有着些許的位移,所以需要通過一系列矩陣變換,把當前幀在視錐內的位置變換到前一幀的視錐中,再對前一幀得到的結果進行取樣。
到這里,我們得到了每個點的顏色。這里覺得可以淺顯的理解為,這個顏色就是當前格大氣被光照之后散射的顏色,相當於一個小光源(的顏色)。但到現在我們只得到了攝像機前每個格子的光源顏色,還沒有計算這些光源照到攝像機中的效果。
就不貼代碼了,太長而且零零散散(
所以第二步就是把每個光源的顏色進行累加啦。
對應的文件為 ComputeAccumulationComputeShader.compute (Accumulation就是累加的意思):
在累加的過程中,我們需要考慮到散射光源在傳播路徑上的衰減:使用exp(指數)函數作為衰減函數(因為 exp( ax ) 是 x = a * x' (經過單位距離衰減a, a < 1) 的解)。過程也很簡單:
1. 每一個thread(threadIdx為x, y, z)計算由當前格子(x, y, z)開始,光線傳播到攝像機(x, y, 0)后的最終顏色。這一過程同時考慮到了路上所有格子的光照(不單只有起始點一個格子)。
2. 通過循環計算光從遠端傳遞到攝像機的過程。Aura代碼里的循環寫得有些繞,是從攝像機(z = 0)循環到當前格的z (從攝像機到當前格方向),然而在計算的時候反過來算衰減,實際上就是從當前格衰減、傳播到攝像機的過程。
half4 Accumulate(half4 colorAndDensityFront, half4 colorAndDensityBack) { half transmittance = exp(colorAndDensityBack.w * layerDepth); half4 accumulatedLightAndTransmittance = half4(colorAndDensityFront.xyz + colorAndDensityBack.xyz * (1.0f - transmittance) * colorAndDensityFront.w, colorAndDensityFront.w * transmittance); // 注意這里 Front + Back * (1.0f - transmittance), 也就是反過來計算。 return accumulatedLightAndTransmittance; } [numthreads(NUM_THREAD_X,NUM_THREAD_Y,NUM_THREAD_Z)] void RayMarchThroughVolume(uint3 id : SV_DispatchThreadID) { // 獲得當前點坐標 half3 normalizedLocalPos = GetNormalizedLocalPositionWithDepthBias(id); #if ENABLE_OCCLUSION_CULLING // 遮擋剔除 [branch] if(IsNotOccluded(normalizedLocalPos.z, id.xy)) // TODO : MAYBE COULD BE OPTIMIZED BY USING A MASK VALUE IN THE DATA TEXTURE #endif { // 設置初值 half4 currentSliceValue = half4(0, 0, 0, 1); half4 nextValue = 0; // 循環計算衰減 [loop] for(uint z = 0; z < id.z; ++z) { nextValue = SampleLightingTexture(uint3(id.xy, z)); currentSliceValue = Accumulate(currentSliceValue, nextValue); } half4 valueAtCurrentZ = SampleLightingTexture(id); currentSliceValue = Accumulate(currentSliceValue, valueAtCurrentZ); // 將最終得到的結果寫入3D紋理 WriteInOutputTexture(id, currentSliceValue); } }
最后一步就是根據最后得到的衰減累加3D紋理,用 PostProcessShader.shader 給圖像做最后的上色。方法也很簡單,獲得當前位置的深度,轉換到對應的 View Space (視錐體)坐標,從3D紋理中采樣,加入一些噪聲之后把當前顏色(計算得到的顏色)疊加到原有圖像上面,就得到了最終的結果:
float4 Aura_GetFogValue(float3 screenSpacePosition) { // Aura_VolumetricLightingTexture: 衰減累加得到的3D紋理(ComputeShader的最終結果) return tex3Dlod(Aura_VolumetricLightingTexture, float4(screenSpacePosition.xy, Aura_RescaleDepth(screenSpacePosition.z), 0)); } void Aura_ApplyFog(inout float3 colorToApply, float3 screenSpacePosition) { // 加入一些噪聲 screenSpacePosition.xy += GetBlueNoise(screenSpacePosition.xy, 3).xy; float4 fogValue = Aura_GetFogValue(screenSpacePosition); // 再加一點噪聲 float4 noise = GetBlueNoise(screenSpacePosition.xy, 4); // 疊加顏色 colorToApply = colorToApply * (fogValue.w + noise.w) + (fogValue.xyz + noise.xyz); } fixed4 frag (v2f psIn) : SV_Target { // 轉換深度坐標 float depth = tex2D(_CameraDepthTexture, psIn.uv); depth = LinearEyeDepth(depth); // _MainTex 為普通渲染得到的最終圖像 float4 backColor = tex2D(_MainTex, psIn.uv); Aura_ApplyFog(backColor.xyz, float3(psIn.uv, depth)); // 見上面的函數定義 return backColor; }
累加前與累加后的對比:

↑ 累加前

↑ 累加后,注意光束
然后我把它隨便丟到了一個場景里面www效果還可以ww?因為還沒有細調www(所以就看看玩玩(x
因為整個操作是在 View Space 的一定范圍里做的,所以場景多大都可以,也算是一個比較讓人滿意的點www?嘛x

以及這邊的累加的作用可以看得更明確一點ww(雖然好像本身就很直觀了:

↑ 累加前

↑ 累加后
以上ww
