zoukankan      html  css  js  c++  java
  • Unity 大气特效插件分析

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

  • 相关阅读:
    寒假Day31:CSU1508地图的四着色bfs+dfs
    寒假Day32:链式前向星
    寒假Day30:HDU4507吉哥系列故事恨7不成妻数位dp
    寒假Day35:HTML表格+图像+超链接
    寒假Day35:2018蓝桥杯 B组
    寒假Day33:HTML入门
    寒假Day30:二叉树的遍历相关题型(求先序或后序、搜索树的判断)
    POJ 1177 Picture (线段树+离散化+扫描线) 详解
    MFC对话框中文出现乱码的解决方法
    如何枚举系统COM串口
  • 原文地址:https://www.cnblogs.com/betairy-linkzeldagg/p/9527315.html
Copyright © 2011-2022 走看看