老实说我也不知道该叫啥了这标题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