跳转至

Unity实现体积光照散射 (Volumetric Light Scattering,云隙光)

原理

Volumetric Light Scattering 的原理可以参考《GPU Gems 3》第13章,书上有效果图:

好看吧,那好,我们的目标就是实现这种效果。

书上介绍了原理,一条关键的公式是:

\[ L(s, \theta, \phi) = exposure \times \sum_{i=0}^n decay^i \times weight \times \frac{L( s_i, \theta_i )}{n} \]

我的理解是,对于图像上的每个像素,光线都有可能照射到,那么对该像素到光源(在投影到图像上的位置)的连线进行采样(对应公式上\(i\)),采样出的结果进行加权平均(对应公式上\(\sum\))并作为该像素的新的颜色值。另外还有关键的后置像素着色器,但是如果只是用那个着色器来对相机渲染的结果进行处理,会产生明显的人工痕迹,有许多的条纹:

那么书上的效果是怎么做出来的?其实书上已经给出了答案,可以用一组图来阐述:

图a 就是粗糙的效果,细心地可以看到有许多条纹,并且没有遮挡不够真实,b、c、 d就是为了获得好的效果需要进行的步骤:

b. 把灯光辐射效果渲染到图像上,并加上物体的遮挡

c. 对b执行 Volumetric Light Scattering 像素着色器,得到遮挡后的效果

d. 添加上把真实场景的颜色

那么下面我们就来一步一步地实现。

画遮挡物体

在实际的操作中,我先用RenderWithShader来把会发生遮挡的物体画成黑色,其他地方为白色,因为这需要对每个面片进行渲染,因此对于复杂的场景,会带来一定的性能消耗。场景中的物体有不透明和透明的,我们希望不透明的物体产生完全的光线遮挡,而透明的物体应该产生部分的遮挡,那么我们就需要针对不同RenderType的物体写不同的Shader,RenderType是SubShader的Tag,不清楚的话可以看这里,写好之后调用:

camera.RenderWithShader(objectOcclusionShader, "RenderType");
RenderWithShader的第二个参数就是要求根据RenderType来替换Shader,简单来说,同一个物体的替换的Shader的RenderType要跟替换前一致,这样我们就可以为不同的RenderType的物体使用不同的Shader:
Shader "Custom/ObjectOcclusion" 
{
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader 
    {
        Tags 
        {
            "Queue" = "Geometry"
            "RenderType" = "Opaque"
        }
        LOD 200
        Pass
        {
            Lighting Off
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;

            v2f_img vert(appdata_img i)
            {
                v2f_img o;
                o.pos = mul (UNITY_MATRIX_MVP, i.vertex);
                return o;
            }

            half4 frag(v2f_img i): COLOR
            {
                return half4(0, 0, 0, 1);
            }
            ENDCG
        }

    }
        SubShader 
    {
        Tags 
        {
            "Queue" = "Geometry"
            "RenderType" = "Transparent"
        }
        LOD 200
        Pass
        {
            Lighting Off
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            Blend SrcAlpha OneMinusSrcAlpha     // blend for transparent objects
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;

            v2f_img vert(appdata_img i)
            {
                v2f_img o;
                o.pos = mul (UNITY_MATRIX_MVP, i.vertex);
                o.uv = MultiplyUV( UNITY_MATRIX_TEXTURE0, i.texcoord );
                return o;
            }

            half4 frag(v2f_img i): COLOR
            {
                half3 output = (1, 1, 1);
                half4 color = tex2D(_MainTex, i.uv);
                half alpha = color.a;
                return half4(output *(1-alpha), alpha);
            }
            ENDCG
        }

    }

    FallBack "Diffuse"
}

注意不透明和透明物体的Shader间的差别:不透明的物体直接画成黑色;不透明物体需要执行blending,获取物体纹理上的alpha通道,并基于这个alpha进行blending。上面代码只是列举了Opaque和Transparent,另外还有TreeOpaque (Shader跟Opaque一样,只是改变RenderType) ,TreeTransparentCutout (同Transparent) 等。由于指定了RenderType,所以为了全面,需要尽可能穷尽场景中的会发生遮挡的物体,我这里就只有前面提到的四种。结果大致如下:

结合物体遮挡画光源辐射

画光源的辐射不难,需要注意的是需要根据屏幕的大小做一些处理,使得光源的辐射状是圆形的:

Shader "Custom/LightRadiate" 
{
    Properties 
    {
        _MainTex ("Base (RGB)", RECT) = "white" {}
        _LightPos ("Light Pos In Screen Space(XY)", Vector) = (0, 0, 0, 1)
        _LightRadius ("Light radiation radius (Pixel)", Float) = 50
    }
    SubShader 
    {
        Tags { "RenderType"="Opaque" }
        LOD 200
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            float4 _LightPos;
            float _LightRadius;

            v2f_img vert(appdata_img i)
            {
                v2f_img o;
                o.pos = mul (UNITY_MATRIX_MVP, i.vertex);
                o.uv = MultiplyUV( UNITY_MATRIX_TEXTURE0, i.texcoord );
                return o;
            }

            half4 frag(v2f_img i): COLOR
            {
                half2 deltaTexCoord = (i.uv - _LightPos.xy) * half2(_ScreenParams.x, _ScreenParams.y);
                float dis = dot(deltaTexCoord, deltaTexCoord);
                const float maxDis = _LightRadius * _LightRadius;
                dis = saturate((maxDis-dis) / maxDis * 0.5);
                return half4(dis, dis, dis, 1) * half4(tex2D(_MainTex, i.uv).rgb, 1);               
            }

            ENDCG
        }
    } 
    FallBack "Diffuse"
}

这个Shader需要输入光源在屏幕上的位置(可以用camera.WorldToViewportPoint来计算,得到的是uv坐标),然后根据指定的半径画一个亮度往外衰减的圆,并把结果跟前面得到的物体遮挡图像(放在_MainTex里)结合,结果大致为:

Light Scattering处理,并结合真实颜色

这里就要用到书上提供的Pixel Shader,我的版本:

Shader "Custom/LightScattering" 
{
    Properties 
    {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _LightRadTex("Light Radiate Tex (RGB)", 2D) = "white" {}
        _LightPos ("Light Pos In Screen Space(XY)", Vector) = (0, 0, 0, 1)
        _Params("Density Weight Decay Exposure", Vector) = (1.0, 1.0, 1.0, 1.0)
    }
    SubShader 
    {
        LOD 200
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            Fog { Mode off }    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0
            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform sampler2D _LightRadTex;
            uniform float4 _LightPos;
            uniform float4 _Params;

            v2f_img vert(appdata_img i)
            {
                v2f_img o;
                o.pos = mul (UNITY_MATRIX_MVP, i.vertex);
                o.uv = MultiplyUV( UNITY_MATRIX_TEXTURE0, i.texcoord );
                return o;
            }

            half4 frag(v2f_img i): COLOR
            {   
                // Calculate vector from pixel to light source in screen space
                float2 deltaTexCoord = (i.uv - _LightPos.xy);

                // Divide by number of samples and scale by control factor, here I use 32 samples
                deltaTexCoord *= 1.0f / 32 * _Params.x; //density;

                // Store color.
                half3 color = tex2D(_MainTex, i.uv).rgb;

                // Store initial sample.
                half3 light = tex2D(_LightRadTex, i.uv).rgb;

                // Set up illumination decay factor.
                half illuminationDecay = 1.0f;

                for(int j = 0; j < 31; ++j)
                {
                    // Step sample location along ray.
                    i.uv -= deltaTexCoord;

                    // Retrieve sample at new location.
                    half3 sample = tex2D(_LightRadTex, i.uv).rgb;

                    // Apply sample attenuation scale/decay factors.
                    sample *= illuminationDecay * 0.03125 * _Params.y ; //weight;

                    // Accumulate combined light.
                    light += sample;

                    // Update exponential decay factor.
                    illuminationDecay *= _Params.z;             //decay;
                }

                // Output final color with a further scale control factor.
                return half4(color+(light * _Params.w), 1); // exposure
            }

            ENDCG       
        }

    } 
    FallBack "Diffuse"
}

大体上跟书上的一致,只是我的参数需要在程序中传进来,并且结合了真实的颜色图和Light Scattering图,结果:

完整代码

代码在这里,把cs脚本添加到相机上。

原文地址:https://wiki.disenone.site

本篇文章受 CC BY-NC-SA 4.0 协议保护,转载请注明出处。