UnityShader——挺进体积光

原本是想找找体积雾的,无意中发现了 GPU Gems 3上的一篇屏幕特效实现体积光散射的文章,实现了一波发现文章里公式列的很高大上,结果具体实现却出乎意料的简单,只是通过像素的屏幕位置和光源的屏幕位置计算光线方向,然后在一个循环中沿光线方向将上一个像素的颜色衰减后叠加到下一个像素,具体可参考 GPU Gems 原文,在 Unity 中效果如下:


UnityShader——挺进体积光_第1张图片


周末又在Github发现一个500多星星的体积光,项目主要参考了 GPU Pro 5 上的一个体积光实现方法,效果很棒,忍不住下下来研究了一下


UnityShader——挺进体积光_第2张图片

我们从最简单并具有代表性的一种情况入手,即当相机在点光源范围外的时候,效果如上图所示,通过绘制一个和点光源范围一样的球,通过raytrace的方法遍历球中的位置并计算其光照,raytrace 路径如图所示,我们只需要遍历光线在球内部的位置:


UnityShader——挺进体积光_第3张图片

实际用使用的代码如下:

                float3 rayStart = _WorldSpaceCameraPos;
                float3 rayEnd = i.wpos;

                float3 rayDir = (rayEnd - rayStart);
                float rayLength = length(rayDir);

                rayDir /= rayLength;

                float3 lightToCamera = _WorldSpaceCameraPos - _LightPos;

                float b = dot(rayDir, lightToCamera);
                float c = dot(lightToCamera, lightToCamera) - (_VolumetricLight.z * _VolumetricLight.z);

                float d = sqrt((b*b) - c);
                float start = -b - d;
                float end = -b + d;

光看代码有点不好理解,示意图如下:


UnityShader——挺进体积光_第4张图片

我们以 C 表示相机所在点, L 表示点光源所在点,根据上面代码我们可以看出,

b=CA
c=CL2DL2
d2=b2c=CA2CL2+DL2=DL2(CL2CA2)=DL2LA2=BL2LA2=AB2

d=AB (上面的计算完全就当高中几何算了就没管方向了)

因此,在点光源范围内没有遮挡物的情况下 -b-d 和 -b+d 分别就是raytrace的起点和终点,再通过 _CameraDepthTexture 计算遮挡物位置并与当前的 end 做比较取最小值,然后,我们就可以做 raytrace了,对于点光源,光照计算较为简单,光线的衰减 Unity 已经帮我们算好了,其具体实现在 Wiki 中也有,然后是阴影,对于点光源,其 ShadowMap 以 texCUBE 的形式存储, UnityShadowLibrary.cginc 文件中,我们可以看到其实现:

#if defined (SHADOWS_CUBE)

samplerCUBE_float _ShadowMapTexture;
inline float SampleCubeDistance (float3 vec)
{
    #ifdef UNITY_FAST_COHERENT_DYNAMIC_BRANCHING
        return UnityDecodeCubeShadowDepth(texCUBElod(_ShadowMapTexture, float4(vec, 0)));
    #else
        return UnityDecodeCubeShadowDepth(texCUBE(_ShadowMapTexture, vec));
    #endif
}
inline half UnitySampleShadowmap (float3 vec)
{
    float mydist = length(vec) * _LightPositionRange.w;
    mydist *= 0.97; // bias

    #if defined (SHADOWS_SOFT)
        float z = 1.0/128.0;
        float4 shadowVals;
        shadowVals.x = SampleCubeDistance (vec+float3( z, z, z));
        shadowVals.y = SampleCubeDistance (vec+float3(-z,-z, z));
        shadowVals.z = SampleCubeDistance (vec+float3(-z, z,-z));
        shadowVals.w = SampleCubeDistance (vec+float3( z,-z,-z));
        half4 shadows = (shadowVals < mydist.xxxx) ? _LightShadowData.rrrr : 1.0f;
        return dot(shadows,0.25);
    #else
        float dist = SampleCubeDistance (vec);
        return dist < mydist ? _LightShadowData.r : 1.0;
    #endif
}

#endif // #if defined (SHADOWS_CUBE)

GitHub 中的项目选择了手动计算衰减,而实际上我们直接使用 UNITY_LIGHT_ATTENUATION 宏也可以达到一样的效果,在这个衰减的基础上,我们可以再手动加上一个衰减系数方便效果调整。
然后是散射,我们之所以能看见“体积光”就是因为光线在介质中发生了散射,在点光源中,我们可以简单的设置一个系数来决定raytrace的每一步有多少光源散射到了摄像机方向,到了这一步,我们便可以得到下面的结果:


UnityShader——挺进体积光_第5张图片

图中的结果是直接在光源位置放置了一个等大小的 Sphere,但实际上还存在一个很大的问题,要产生阴影,我们则不能将其渲染队列设置为透明,但在 Geometry 的渲染队列绘制,如果我们渲染天空盒,则体积光便会被天空盒覆盖掉,如图所示:


UnityShader——挺进体积光_第6张图片
把渲染队列调整至 Transparent 之后则是这样的:

UnityShader——挺进体积光_第7张图片

因此,Github 中的项目使用了 commandbuffer 在 Unity 渲染完 ShadowMap 后,绘制不透明物体之前绘制体积光的Sphere 并将rendertarget 设置为自定义的 rendertexture,最后再通过ImageEffect的方式,将体积光混合到当前的 framebuffer 中


UnityShader——挺进体积光_第8张图片

当然,上面提到的都是项目中比较基础的部分,Github 项目中还使用了 Deithered Offset 的方法来使 raytrace 的采样更加均匀,可以在使用更少的步数的情况下,达到更好的效果


UnityShader——挺进体积光_第9张图片

以及先降采样再通过模糊和超采样还原的方法来减少运算量


UnityShader——挺进体积光_第10张图片

这里不再一一分析,有兴趣的可以查看 GPU Pro 原文或者阅读 Github 的项目源码


to be continue… or not

你可能感兴趣的:(U3D)