跳轉至

Unity實現體積光照散射(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執行體積光散射像素著色器,得到遮蔽後的效果

在图上添加真实场景的颜色

那麼接下來,讓我們一步一步地實現。

遮擋物體

在實際操作中,我先使用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內)結合,結果大致為:

光散射處理,並結合真實顏色。

這裡將使用書中提供的像素着色器,請參考我的版本:

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腳本添加到相機上。

Original: https://wiki.disenone.site/tc

This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.

這篇文章是透過 ChatGPT 翻譯的,如果您有任何反饋指出任何遺漏之處。