UP | HOME

VolumetricLightScattering

Table of Contents

VolumetricLightScattering note.

<!– more –>

VolumetricLightScattering

Basic Knowledge

体渲染相关概念

吸收(absorption):光能转化为介质内其它形式的能(如热能)。
外散射(out-scattering):光打在介质粒子上散射到其它方向去了。
自发光(emission):介质内其它形式的能(如热能)转化成光能。
内散射(in-scattering):其它方向来的光打在介质粒子上恰好散射到本方向上。

体渲染数学原理 https://zhuanlan.zhihu.com/p/56710440

丁达尔效应

可见光的波长约在 400~700 nm 之间,当光线射入分散体系时,一部分自由地通过,一部分被吸收、反射或散射,可能发生以下三种情况:
(1)当光束通过粗分散体系,由于分散质的粒子大于入射光的波长,主要发生反射或折射现象,使体系呈现混浊。
(2)当光线通过胶体溶液,由于分散质粒子的直径一般在 1~100 nm 之间,小于入射光的波长,主要发生散射,可以看见乳白色的光柱,出现丁达尔现象。
(3)当光束通过分子溶液,由于溶液十分均匀,散射光因相互干涉而完全抵消,看不见散射光。

瑞利散射

瑞利散射是由远小于辐射波长的粒子对光或其他电磁辐射的散射。散射量与光的波长的四次方成反比(波长越短 Rayleigh 散射越强)。

波长 = 光速 * 1/频率

空气分子对光的散射就属于 Rayleigh Scattering,瑞利散射是各向同性的,吸收可以忽略。

太阳光谱中波长较短的蓝紫光比波长较长的红光散射更明显,而短波中又以蓝光能量最大,所以在雨过天晴或秋高气爽时(空中较粗微粒比较少,以分子散射为主),在大气分子的强烈散射作用下,蓝色光被散射至弥漫天空,天空即呈现蔚蓝色。

米氏散射

当大气中粒子的直径与辐射的波长相当时发生的散射。这种散射主要由大气中的微粒,如烟、尘埃、小水滴及气溶胶等引起。米氏散射的散射强度与波长的二次方成反比,并且散射在光线向前方向比向后方向更强,方向性比较明显。

米氏散射是各向异性的,具有很强的 forward lobe 和更高的吸收比例。

LightShaft

RayMarching LightShaft 0

  1. 渲染光源方向的深度图
  2. 渲染摄像机方向的深度图
  3. 以摄像机原点为射线原点,以摄像机到当前着色点方向为射线方向,以摄像机深度为射线最远距离。
  4. 若当前 RayMarching 点不在阴影中,并且 RayMarching 点在光源范围内,则表示该点对 LightShaft 效果有贡献,否则无贡献。
    • 将当前 RayMarching 点转化到光源空间内,若 RayMarching 点的深度 <= 光源空间 DepthRT 深度,则表示其不在阴影中。

Tips:
方向光范围是无限大,所以只需要考虑 RayMarching 点是否在阴影中
Point Light 和 Spot Light 的范围都是有限的,需要考虑 RayMarching 点是否在光源范围内

god-ray.jpg

god-ray2.jpg

下面代码实现了方向光的体积光效果:

Shader "Hidden/Toguchi/PostProcessing/LightShaft"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

HLSLINCLUDE
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
    #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

    TEXTURE2D_X(_MainTex);
    TEXTURE2D_X_FLOAT(_CameraDepthTexture);
    SAMPLER(sampler_CameraDepthTexture);

    TEXTURE2D_X(_LightShaftTempTex);

    half4 _MainTex_ST;
    float4 _MainTex_TexelSize;

    float4 _CamWorldSpace;
    float4x4 _CamFrustum, _CamToWorld;
    int _MaxIterations;
    float _MaxDistance;
    float _MinDistance;

    float _Intensity;

    struct RayVaryings
    {
        float4 positionCS    : SV_POSITION;
        float2 uv            : TEXCOORD0;
        float4 ray           : TEXCOORD1;
    };

    RayVaryings Vert_Ray(Attributes input)
    {
        RayVaryings output;
        output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
        output.uv = input.uv;

        int index = output.uv.x + 2 * output.uv.y;
        output.ray = _CamFrustum[index];

        return output;
    }

    float GetRandomNumber(float2 texCoord, int Seed)
    {
        return frac(sin(dot(texCoord.xy, float2(12.9898, 78.233)) + Seed) * 43758.5453);
    }

    half4 SimpleRaymarching(float3 rayOrigin, float3 rayDirection, float depth)
    {
        half4 result = float4(_MainLightColor.xyz, 1) * _Intensity;

        float step = _MaxDistance / _MaxIterations;
        float t = _MinDistance + step * GetRandomNumber(rayDirection, _Time.y * 100);
        float alpha = 0;

        for(int i = 0; i < _MaxIterations; i++)
        {
            if(t > _MaxDistance || t >= depth)
            {
                break;
            }

            float3 p = rayOrigin + rayDirection * t;

            float4 shadowCoord = TransformWorldToShadowCoord(p);
            float shadow = SAMPLE_TEXTURE2D_SHADOW(_MainLightShadowmapTexture, sampler_MainLightShadowmapTexture, shadowCoord);
            if(shadow >= 1) // 当前点不在阴影中
            {
                alpha += step * 0.2;
            }

            t += step;
        }

        result.a *= saturate(alpha);

        return result;
    }

    half4 Frag(RayVaryings input) : SV_Target
    {
        float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_LinearClamp, input.uv).r;
        depth = Linear01Depth(depth, _ZBufferParams);
        depth *= length(input.ray);

        float3 rayOrigin = _CamWorldSpace;
        float3 rayDir = normalize(input.ray);
        float4 result = SimpleRaymarching(rayOrigin, rayDir, depth);

        return result;
    }

    half4 Frag_Combine(Varyings input) : SV_Target
    {
        half4 color = SAMPLE_TEXTURE2D_X(_MainTex, sampler_LinearClamp , input.uv);
        half4 shaft = SAMPLE_TEXTURE2D_X(_LightShaftTempTex, sampler_LinearClamp, input.uv);

        color.rgb = color.rgb * (1 - shaft.a) + shaft.rgb * shaft.a;
        return color;
    }
ENDHLSL

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off

        Pass
        {
            Name "GradientFog"

            HLSLPROGRAM
                #pragma vertex Vert_Ray
                #pragma fragment Frag
            ENDHLSL
        }

        Pass
        {
            Name "Combine"

            HLSLPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag_Combine
            ENDHLSL
        }
    }
}

RayMarching LightShaft 1

  1. 渲染光源以及遮挡物到 OcclusionRT
  2. 正常渲染场景到 framebuffer
  3. 以远离光源的方向进行 RayMarching。Occlusion 按照远离距离衰减,将 Occlusion 和 FrameBuffer 进行混合。
    • 混合函数为 glBlendFunc(GL_SRC_ALPHA, GL_ONE);

fake-god-ray-01.jpg

uniform float exposure;
uniform float decay;
uniform float density;
uniform float weight;
uniform vec2 lightPositionOnScreen;
uniform sampler2D firstPass;  // occlusion rt
const int NUM_SAMPLES = 100 ;

void main()
{
    vec2 deltaTextCoord = vec2( gl_TexCoord[0].st - lightPositionOnScreen.xy );
    vec2 textCoo = gl_TexCoord[0].st;
    deltaTextCoord *= 1.0 /  float(NUM_SAMPLES) * density;
    float illuminationDecay = 1.0;


    for(int i=0; i < NUM_SAMPLES ; i++)
    {
        textCoo -= deltaTextCoord;
        vec4 sample = texture2D(firstPass, textCoo );

        sample *= illuminationDecay * weight;

        gl_FragColor += sample;

        illuminationDecay *= decay;
    }
    gl_FragColor *= exposure;
}

light scattering with opengl https://fabiensanglard.net/lightScattering/ 有道云备份

RayMarching LightShaft 2

  1. 屏幕空间内,以当前着色点 pScreenUV 为原点,以光源着色点 lightScreenUV 为目标点。在这两个点之间进行 RayMarching。
  2. 若当前 RayMarching 点对应的深度为 1,则表示该点必定不在阴影中,其对 LightShaft 效果有贡献,否则无贡献。
    • 近似计算 RayMarching 点对 LightShaft 的贡献

god-ray3.jpg

Unity GodRay https://zhuanlan.zhihu.com/p/144037609

UE4 LightShafts

Occlusion 方法
利用场景深度 RT 创建一个 Mask,以远离 light 的方向对该 Mask 进行多次 Blur 操作,将结果作为 fog 和大气的 mask。该方法和现实生活中的光束产生的原理类似 - 光束由雾气的阴影所生成。注意:这也意味着光束的强弱度只能和雾气和大气相同。原理同上面 RayMarching LightShaft 1

Bloom 方法
该方法在世界空间中的光源周围捕捉场景颜色(包括半透明度和雾气散射),并从光源进行径向模糊。此法并非对真实世界中发生的一切进行模拟,但可控性较高(不受雾气密度限制),视觉效果震撼。太阳周围存在突出的明亮区域(如明亮的云朵)时光晕法的使用效果最佳。明亮的太阳过小,会形成一定程度的锯齿,而且 Blur 操作是图像空间操作,消耗比较高。

Volumetric Light Beam

源码分析
根据 CameraDepthRT 计算屏幕上某点到摄像机的距离
inline float Depth_PS_GetSceneDepthFromEye(float4 uv, float3 posViewSpace)
{
    // rawDepth: depth rt中存储的depth值 [0, 1], non linear
    float rawDepth = VLBSampleDepthTexture(uv);
    // linearDepthPersp: 当前像素点的camera space 下的z值
    float linearDepthPersp = VLBLinearEyeDepth(rawDepth);

    // transform perspective depth from near plane to distance based on the eye
    // acosViewDirZ: 当前像素点方向 和 摄像机方向的夹角的 cos 值
    float acosViewDirZ = abs(normalize(posViewSpace.xyz).z); // TODO precompute that in VS?
    // linearDepthPersp: 当前像素点到 camera 的距离
    linearDepthPersp /= acosViewDirZ;

    float linearDepthOrtho = Depth_PS_GetLinearDepthOrtho(rawDepth);
    return lerp(linearDepthPersp, linearDepthOrtho, VLB_CAMERA_ORTHO);
}