[Unity3D][ImageEffect]Kino bloom filterを見る

高橋啓二朗(@_kzr)氏のKino Bloomフィルターの中身をみてみる。

Bloom

Bloom.csとshaderを行ったり来たりしながら見ていく。

void OnRenderImage(RenderTexture source, RenderTexture destination)

MonoBehaviour.OnRenderImage(RenderTexture,RenderTexture)

メインとなる関数。
第一引数がこれまでの結果。第二引数にこの関数で変更した内容を適用してあげなければならない。

適用はGraphics.Blitで行う。

    Graphics.Blit( source , dest );
Ad

この関数はあくまで第一引数の内容を第二引数に書き込むだけなので、今回のOnRenderImage内の場合は、OnRenderImage()の第二引数をこの Graphics.Blit() の第二引数に入れて書き込んで上げる必要がある。

  • OnRenderImage(RenderTexture,RenderTexture) は全てのレンダリングが終わった時に呼ばれるカメラ用イベント。
  • なのでコンポーネントには [RequireComponent( typeof(Camera ))] が付いてる

動作

    if (_material == null)
    {
        _material = new Material(_shader);
        _material.hideFlags = HideFlags.DontSave;
    }
    var logh = Mathf.Log(source.height, 2) + _radius - 5;
    var logh_i = (int)logh;
    var iteration = Mathf.Max(2, logh_i);
  • Math.Log() で logarythm RenderTexture souce の 高さの対数を2を底としてとっている。
  • そこに半径となる値(Editorで5が最大値)から-5した値を加算。つまり( -5 ~ 0 )を加算。
  • iterationは後で使う繰り返し回数。最低でも2回行う。
    // update the shader properties
    _material.SetFloat("_SampleScale", 0.5f + logh - logh_i);
    _material.SetFloat("_Intensity", _intensity);

    var pf = -Mathf.Log(_exposure * 0.998f + 0.001f);
    _material.SetFloat("_Prefilter", pf);

    if (_antiFlicker)
        _material.EnableKeyword("PREFILTER_MEDIAN");
    else
        _material.DisableKeyword("PREFILTER_MEDIAN");


shader側にパラメータを付与。
– Propertyで定義してなくてもパラメータを渡せることを初めて知った…。
– Bloom.shader に定義してある パラメータ “PREFILTER_MEDIAN” をスイッチさせている!!!!

    // allocate temporary buffers
    var rt1 = new RenderTexture[iteration + 1];
    var rt2 = new RenderTexture[iteration + 1];

    var tx = source.width;
    var ty = source.height;

    for (var i = 0; i < iteration + 1; i++)
    {
        rt1[i] = GetTempBuffer(tx, ty);
        if (i > 0 && i < iteration)
            rt2[i] = GetTempBuffer(tx, ty);
        tx /= 2;
        ty /= 2;
    }

仮バッファを用意。
ループが回る都度、解像度を半分にしている。後の処理のコメントにもかかれてるけどMipMap用。
rt2 は最後と最初だけスキップ。

このローカル関数で一時的なレンダリングテクスチャを割当。
フォーマットは HDR でのプラットフォームによるデフォルトのフォーマット

        RenderTexture GetTempBuffer(int width, int height)
        {
            return RenderTexture.GetTemporary(
                width, height, 0, RenderTextureFormat.DefaultHDR);
        }

    // apply the prefilter
    Graphics.Blit(source, rt1[0], _material, 0);
  • Sourceを rt1[0] の RenderTexture に書き込む。
  • _material の pass 0 を使って描画。

ちょっとお試し。

rt1[0]

最初のPass

次の時点だけで中断。

    // apply the prefilter
    Graphics.Blit(source, rt1[0], _material, 0);

    Graphics.Blit( rt1[0], destination );
    return; // 処理を強制終了して描画状況を確認する。

https://gyazo.com/6ea728a7963a05e30cdf0667918697df

Shader側では、

    half4 frag_prefilter(v2f_img i) : SV_Target
    {
#if PREFILTER_MEDIAN
        float3 d = _MainTex_TexelSize.xyx * float3(1, 1, 0);

        half4 s0 = tex2D(_MainTex, i.uv);
        half3 s1 = tex2D(_MainTex, i.uv - d.xz).rgb;
        half3 s2 = tex2D(_MainTex, i.uv + d.xz).rgb;
        half3 s3 = tex2D(_MainTex, i.uv - d.zy).rgb;
        half3 s4 = tex2D(_MainTex, i.uv + d.zy).rgb;

        half3 m = median(median(s0.rgb, s1, s2), s3, s4);
#else
        half4 s0 = tex2D(_MainTex, i.uv);
        half3 m = s0.rgb;
#endif

        m *= smoothstep(0, _Prefilter, Luminance(m));

        return half4(m, s0.a);
    }
  • PREFILTER_MEDIAN は先のパラメータでスイッチされている。
  • _Prefilter として受け取ったパラメータ _exposure のパラメータをつかって smoothstep() 関数ででなだらかに。
  • Mathf.SmoothStep()と同じと思われる。ちゃんと読んでない。 UnityCG.cginc を要確認。
    half3 median(half3 a, half3 b, half3 c)
    {
        return a + b + c - min(min(a, b), c) - max(max(a, b), c);
    }

この関数で突出しすぎる値を抑えてるのかな?

rt1[3]

2つ目。

    // create a mip pyramid
    for (var i = 0; i < iteration; i++)
        Graphics.Blit(rt1[i], rt1[i + 1], _material, 1);


    Graphics.Blit( rt1[3], destination );
    return; // 処理を強制終了して描画状況を確認する。

ループで、テクスチャ解像度の低いバッファに書き込んでいっている。

https://gyazo.com/a528eb670ca94c45623ac10e7e2573c0

ボカシ具合2。

Shader側の処理は以下のとおり。

    half4 frag_box_reduce(v2f_img i) : SV_Target
    {
        float4 d = _MainTex_TexelSize.xyxy * float4(-1, -1, +1, +1);

        float3 s;
        s  = tex2D(_MainTex, i.uv + d.xy).rgb;
        s += tex2D(_MainTex, i.uv + d.zy).rgb;
        s += tex2D(_MainTex, i.uv + d.xw).rgb;
        s += tex2D(_MainTex, i.uv + d.zw).rgb;

        return half4(s * 0.25, 0);
    }

rt1[最後]

    // blur and combine loop
    _material.SetTexture("_BaseTex", rt1[iteration - 1]);
    Graphics.Blit(rt1[iteration], rt2[iteration - 1], _material, 2);

    Graphics.Blit( rt2[iteration - 1], destination );
    return; // 処理を強制終了して描画状況を確認する。

https://gyazo.com/fc4b5072379f618d2c04585680980881

ボカシがかなり強く入ってる。解像度が一番低いバッファなので当然ではあるのだけど…。

    _material.SetTexture("_BaseTex", rt1[iteration - 1]);

ここで、最終的に解像度の一番低いテクスチャをマテリアルに渡す。

rt2[x]

    for (var i = iteration - 1; i > 1; i--)
    {
        _material.SetTexture("_BaseTex", rt1[i - 1]);
        Graphics.Blit(rt2[i],  rt2[i - 1], _material, 2);
    }


    Graphics.Blit( rt2[1], destination );
    return; // 処理を強制終了して描画状況を確認する。

解像度の高いバッファに順次書き込み。

最終的に…

https://gyazo.com/9608f8a9fee21589462be8198fb2092e

まで到達。

shader側。

    half4 frag_tent_expand(v2f_img i) : SV_Target
    {
        float4 d = _MainTex_TexelSize.xyxy * float4(1, 1, -1, 0) * _SampleScale;

        float4 base = tex2D(_BaseTex, i.uv);

        float3 s;
        s  = tex2D(_MainTex, i.uv - d.xy).rgb;
        s += tex2D(_MainTex, i.uv - d.wy).rgb * 2;
        s += tex2D(_MainTex, i.uv - d.zy).rgb;

        s += tex2D(_MainTex, i.uv + d.zw).rgb * 2;
        s += tex2D(_MainTex, i.uv       ).rgb * 4;
        s += tex2D(_MainTex, i.uv + d.xw).rgb * 2;

        s += tex2D(_MainTex, i.uv + d.zy).rgb;
        s += tex2D(_MainTex, i.uv + d.wy).rgb * 2;
        s += tex2D(_MainTex, i.uv + d.xy).rgb;

        return half4(base.rgb + s * (1.0 / 16), base.a);
    }

RenderTextureのUVをずらしてボカシ処理。

最後の描画

一番最後にボカしまくったRenderTextureともともとの source を合体させ、それを distination に書き込む。

    // finish process
    _material.SetTexture("_BaseTex", source);
    Graphics.Blit(rt2[1], destination, _material, 3);

shader 側

    half4 frag_combine(v2f_img i) : SV_Target
    {
        half4 base = tex2D(_BaseTex, i.uv);
        half3 blur = tex2D(_MainTex, i.uv).rgb;
        return half4(base.rgb + blur * _Intensity, base.a);
    }

_Intensity で増幅させたボカシ画像と合成してる。

RenderTextureの開放

生成したしたRenderTextureを開放。

    // release the temporary buffers
    for (var i = 0; i < iteration + 1; i++)
    {
        ReleaseTempBuffer(rt1[i]);
        ReleaseTempBuffer(rt2[i]);
    }

まとめ

  • レンダリング結果の画像を取得し解像度のちがうRenderTextureを用意
  • RenderTextureにボカシ処理をShader経由で行う。
  • ボカシた絵とオリジナルを合成して最終出力。

感想とか学んだこと

  • カメラから送られる OnRenderImage() で最終描画内容を取得できる。
  • イベント中に受け取る RenderTexture をMaterialとアタッチしたShaderを経由して加工できる。
  • ShaderのPass別に処理を複数に分けることができ、必要に応じて適用する関数を変更できる。
  • 人のコードやプロジェクトをみるとむちゃくちゃ勉強になる。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です