UnityShader——挺进体积光

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



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


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


实际用使用的代码以下:ui

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;

光看代码有点很差理解,示意图以下:
google


咱们以 C 表示相机所在点, L 表示点光源所在点,根据上面代码咱们能够看出,
spa

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 文件中,咱们能够看到其实现:3d

#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的每一步有多少光源散射到了摄像机方向,到了这一步,咱们即可以获得下面的结果:
code


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



把渲染队列调整至 Transparent 以后则是这样的:

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


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


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


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


to be continue… or not