質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.59%

ガウシアンブラーの実装

解決済

回答 2

投稿

  • 評価
  • クリップ 1
  • VIEW 4,124

shiroshiro_me

score 19

f(x,y)=1/(2*π*σ)*exp(-(x^2+y^2)/(2*σ^2))

unityのshaderで上記の式を基にしたガウシアンブラーを実装したいと考えています。フィルタサイズは気にしていませんが、σの値をプロパティで変えることで、ブラーの強さを調節するプログラムにしたいと考えています。
しかし、フィルタサイズは3×3で、σの値が不可変のshaderプログラムしか作成できませんでした。どのようにすればσの値が可変な上記の式を実装できるか分からず質問させていただきました。下記は現在のプログラムです。

Shader "Custom/Frost"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)

        _MainTex("Diffuse", 2D) = "white" {}
        _Noise("Noise", 2D) = "black" {}

        _Range("Range", Float) = 0.025
        _Blur("Blur", Float) = 0.005
    }

    SubShader
    {
        Cull Off

        GrabPass{ "_Frost" }

        CGINCLUDE
        #include "UnityCG.cginc"

        half4 _Color;

        sampler2D _MainTex;
        float4 _MainTex_ST;

        sampler2D _Frost;
        sampler2D _Noise;
        float4 _Noise_ST;

        half _Range;
        half _Blur;

        ENDCG

        Pass
        {
            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            struct v2f
            {
                float4 pos : SV_POSITION;

                float3 uv : TEXCOORD;
                float4 screenPos : TEXCOORD1;
                float3 ray : TEXCOORD2;
            };

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.screenPos = ComputeScreenPos(o.pos);
                o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
                float2 uv = i.screenPos.xy / i.screenPos.w;

                float2 frostUV = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw).xy;

                frostUV -= 0.5;
                frostUV *= _Range;
                frostUV += uv;

                half4 frost = 4 * tex2D(_Frost, frostUV);
                frost += tex2D(_Frost, frostUV + float2(_Blur, _Blur));
                frost += 2 * tex2D(_Frost, frostUV + float2(_Blur, 0));
                frost += tex2D(_Frost, frostUV + float2(_Blur, -_Blur));
                frost += 2 * tex2D(_Frost, frostUV + float2(0, _Blur));
                frost += 2 * tex2D(_Frost, frostUV + float2(0, -_Blur));
                frost += tex2D(_Frost, frostUV + float2(-_Blur, _Blur));
                frost += 2 * tex2D(_Frost, frostUV + float2(-_Blur, 0));
                frost += tex2D(_Frost, frostUV + float2(-_Blur, -_Blur));
                frost *= 0.0625;

                half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);

                half alpha = _Color.a * diffuse.a;

                return half4(frost.xyz + (diffuse.rgb * _Color.rgb * alpha), 1);
            }

            ENDCG
        }
    }

    Fallback Off
}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+2

こんな感じでやってみましたが、ご参考になりますでしょうか?

Shader "Custom/Frost"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)

        _MainTex("Diffuse", 2D) = "white" {}
        _Noise("Noise", 2D) = "black" {}

        _Range("Range", Float) = 0.025
        // _Blur("Blur", Float) = 0.005 // Blurは廃止、サンプリング位置ずらしはテクセルサイズに基づいた形に変更
        _Sigma("Sigma", Range(0.01, 8.0)) = 1.0 // σを追加
    }

    SubShader
    {
        // スカイボックスも含めてグラブするため、キューをTransparentに変更
        Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

        Cull Off

        GrabPass{ "_Frost" }

        CGINCLUDE
        #include "UnityCG.cginc"

        half4 _Color;

        sampler2D _MainTex;
        float4 _MainTex_ST;

        sampler2D _Frost;

        // グラブテクスチャのテクセルサイズを追加
        float4 _Frost_TexelSize;

        sampler2D _Noise;
        float4 _Noise_ST;

        half _Range;
        // half _Blur; // _Blurは廃止
        float _Sigma; // _Sigmaを追加

        // 重み計算用関数
        // ご質問者さんの重み関数 1/(2*π*σ)*exp(-(x^2+y^2)/(2*σ^2)) と同様ですが、係数は1に変更しました
        // 係数を付けて正規化した重みで有限の範囲をたたみ込むと、重みの総和が1より小さくなってしまうため
        // できあがったぼかし画像は、もとの画像より暗くなってしまうと思われます
        // そこで重み関数には係数を付けず、たたみ込みの際に重みの総和を求めて、最後にたたみ込み結果を
        // 重みの総和で割ることで明るさが維持されるようにしました
        inline float getWeight(float2 xy)
        {
            return exp(-dot(xy, xy) / (2.0 * _Sigma * _Sigma));
        }

        // カーネルサイズ計算用関数
        // たたみ込み範囲の片側幅...カーネルの一辺の長さ2*n+1のnをいくつにするかですが、さしあたり
        // 中心から最も遠い点(カーネルの角)における重みが十分小さくなる(0.0001を切る)大きさにしました
        // σ=1でn=4となり、サンプリング回数は(2*4+1)^2=81回になります(多分...)
        // σ=2で225回、σ=4で729回、σ=8で2601回...といった具合に、2乗のオーダーでサンプリング回数が
        // 増大しますので、あんまりσを大きくしすぎるのは控えた方がいいでしょう
        // ぼかしを縦方向と横方向の二段階に分けることで、サンプリング回数の増大を1乗のオーダーに
        // 抑えるテクニックもありますので、負荷を軽減したい場合は採用を検討してみてもいいでしょう
        inline int getKernelN()
        {
            return (int)ceil(_Sigma * sqrt(-log(0.0001)));
        }

        ENDCG

        Pass
        {
            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            struct v2f
            {
                float4 pos : SV_POSITION;

                float3 uv : TEXCOORD;
                float4 screenPos : TEXCOORD1;
                float3 ray : TEXCOORD2;
            };

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.screenPos = ComputeGrabScreenPos(o.pos);
                o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                // rayは使用していないようですが、一応残しておきました
                i.ray = i.ray * (_ProjectionParams.z / i.ray.z);

                float2 uv = i.screenPos.xy / i.screenPos.w;

                float2 frostUV = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw).xy;

                frostUV -= 0.5;
                frostUV *= _Range;
                frostUV += uv;

                // 霜のついたガラスを表現するためサンプリング位置にノイズを加えているようですが、
                // もし純粋にぼかし効果だけをかけたい場合は、uvをそのまま使うといいでしょう
                // frostUV = uv;

                int kernelN = getKernelN();
                float weightSum = 0.0;
                float4 frost = 0.0;

                // 注目しているピクセルを中心に、-kernelN ~ +kernelNの範囲をたたみ込む
                for (int m = -kernelN; m <= kernelN; m++)
                {
                    for (int n = -kernelN; n <= kernelN; n++)
                    {
                        float2 texelOffset = float2(n, m);
                        float weight = getWeight(texelOffset);
                        weightSum += weight;
                        frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                    }
                }

                // 最後に、重みの総和で割る
                frost /= weightSum;

                half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);

                half alpha = _Color.a * diffuse.a;

                return half4(frost.xyz + (diffuse.rgb * _Color.rgb * alpha), 1);
            }

            ENDCG
        }
    }

    Fallback Off
}

コード中のコメントでもちょっと言及しましたが、このままではσを大きくすると、毎フレーム描画では重さが気になるかもしれません。
追加のレンダーテクスチャが必要になるためコードはちょっとややこしくなるでしょうが、wgld.org | WebGL: gaussian フィルタ |で紹介されているような縦横二段掛け方式とか、品質とパフォーマンスを両立する独自の技術|YEBIS 3で解説されているような低解像度化してぼかし処理を行うテクニックを用いれば、もっと軽量化できるかもしれませんね。

σ = 1、ノイズ効果なし
1
σ = 1、ノイズ効果あり
F1
σ = 8、ノイズ効果なし
8
σ = 8、ノイズ効果あり
F8

 ループ展開版

非展開版と比べると、シェーダーのインポートに時間がかかるかもしれません。
KERNEL_N_MAXをもっと小さくすると、対応可能なぼかし幅は小さくなってしまうものの、インポート速度・動作速度は改善するかと思います。

Shader "Custom/Frost"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)

        _MainTex("Diffuse", 2D) = "white" {}
        _Noise("Noise", 2D) = "black" {}

        _Range("Range", Float) = 0.025
        _Sigma("Sigma", Range(0.01, 8.0)) = 1.0
    }

    SubShader
    {
        Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

        Cull Off

        GrabPass{ "_Frost" }

        CGINCLUDE
        #include "UnityCG.cginc"

        half4 _Color;

        sampler2D _MainTex;
        float4 _MainTex_ST;

        sampler2D _Frost;
        float4 _Frost_TexelSize;

        sampler2D _Noise;
        float4 _Noise_ST;

        half _Range;
        float _Sigma;

        // 重み計算用関数
        inline float getWeight(float2 xy)
        {
            return exp(-dot(xy, xy) / (2.0 * _Sigma * _Sigma));
        }

        // カーネルサイズ計算用関数
        inline int getKernelN()
        {
            return (int)ceil(_Sigma * sqrt(-log(0.0001)));
        }

        // 最大kernelN...(int)ceil(8 * sqrt(-ln(0.0001)))
        #define KERNEL_N_MAX 25

        ENDCG

        Pass
        {
            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            struct v2f
            {
                float4 pos : SV_POSITION;

                float3 uv : TEXCOORD;
                float4 screenPos : TEXCOORD1;
                float3 ray : TEXCOORD2;
            };

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.screenPos = ComputeGrabScreenPos(o.pos);
                o.ray = UnityObjectToViewPos(v.vertex).xyz * float3(-1, -1, 1);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                i.ray = i.ray * (_ProjectionParams.z / i.ray.z);

                float2 uv = i.screenPos.xy / i.screenPos.w;

                float2 frostUV = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw).xy;

                frostUV -= 0.5;
                frostUV *= _Range;
                frostUV += uv;
                // frostUV = uv;

                int kernelN = getKernelN();

                // ループ展開版
                float2 texelOffset = float2(0, 0);
                float weight = getWeight(texelOffset);
                float weightSum = weight;
                float4 frost = weight * tex2D(_Frost, frostUV);

                [unroll(KERNEL_N_MAX)]
                for (int n = 0; n < kernelN; n++)
                {
                    int x = n + 1;

                    texelOffset = float2(x, 0);
                    weight = getWeight(texelOffset);
                    weightSum += 2 * weight;
                    frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                    texelOffset = float2(-x, 0);
                    frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                }

                [unroll(KERNEL_N_MAX)]
                for (int m = 0; m < kernelN; m++)
                {
                    int y = m + 1;

                    texelOffset = float2(0, y);
                    weight = getWeight(texelOffset);
                    weightSum += 2 * weight;
                    frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                    texelOffset = float2(0, -y);
                    frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);

                    [unroll(KERNEL_N_MAX)]
                    for (int n = 0; n < kernelN; n++)
                    {
                        int x = n + 1;

                        texelOffset = float2(x, y);
                        weight = getWeight(texelOffset);
                        weightSum += 4 * weight;
                        frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                        texelOffset = float2(-x, y);
                        frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                        texelOffset = float2(x, -y);
                        frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                        texelOffset = float2(-x, -y);
                        frost += weight * tex2D(_Frost, frostUV + texelOffset * _Frost_TexelSize.xy);
                    }
                }

                frost /= weightSum;

                half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);

                half alpha = _Color.a * diffuse.a;

                return half4(frost.xyz + (diffuse.rgb * _Color.rgb * alpha), 1);
            }

            ENDCG
        }
    }

    Fallback Off
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/03 07:10 編集

    Windows 10 バージョン1803、Unity 2018.2.10f1、PC, Mac & Linux Standalone <DX11>でやってみたものの、結局実行時にぼかし効果が消失する現象は再現できませんでした(ColorのAlphaを変えても、プレイモード・非プレイモードに関わらずぼかしの効きには影響しない)...

    最初に投稿したコードではループ変数にiとjを使っていましたが、不確かですが変数名がフラグメントシェーダー入力変数のiと名前かぶりしていたのがまずかったかもしれません。念のため、ループ変数の名前をnとmに変えてみました。

    また、ループ部分を展開するよう指示したバージョンも試しに作ってみました。一応全文を投稿しましたが、変更箇所は主にループ周辺のみです。
    ループが最大回数で打ち切られるかもしれなくなったため、たたみ込みは注目ピクセルに近い部分から外側に向かって行い、ループが打ち切られても注目ピクセルがカーネル中心に位置するようにしてみました。

    キャンセル

  • 2018/10/03 13:48

    ループ展開版も試してみましたが、やはり再生時にブラーが消失してしまいました。
    実は現在「FOVE0」というHMDを用いるコンテンツの開発を行っており、もしやと思いFOVEの公式サイトで与えられたFOVE専用のカメラのprefabをoffにし、通常のカメラで再生してみた結果gameビューでブラーの消失は観測されませんでした。
    なので原因はHMDおよびそれ専用の特殊なカメラにありそうです。こちらで色々探ってみます。
    また、このマテリアルを適用したオブジェクトを複数用いる予定のため、すべてオンにしてみた結果、フレームレートがおよそ14fpsに大幅に低下してしまいました。処理を軽くする方法も探りたいと考えています。

    キャンセル

  • 2018/10/05 06:10

    すでにご覧になっているかもしれませんが、同様のケースがないか検索してみたところ、https://support.getfove.com/hc/en-us/community/posts/360014764273-Shader-not-working-Unity-3D- に投稿されている症状がご質問者さんのものとよく似ているように思います。

    投稿者の方が使おうとしたシェーダーもグラブパスで描画内容をキャプチャーし、それにぼかしをかけるもののようで、今回のトラブルと同じ原因なのかもしれません。
    グラブパスがあるのが悪いのか、複数回テクスチャサンプリングを行っているのが悪いのか、あるいは他の原因なのかは、すみませんがFOVEを持っていないため、これについて調査するのは困難そうです。
    Frost軽量化バージョンでは、グラブパスではなくコマンドバッファ挿入で画面キャプチャーを行っていますので、もしグラブパスが原因なら、こちらであればぼかしをかけられるかも知れません。

    残念ながらこの投稿に対する解決策は寄せられていないようですが、回答によるとUnity 2018.xにおいてレンダリング関連のトラブル報告があるようで、もしかするとUnityをダウングレードすれば動くようになるかも...?

    FOVEの問題となるとあまりお力になれそうもなく、せめてと思い軽量化する案について別回答に投稿しました。
    方針としては、こちらの回答で言及した「縦横二段掛け」と「低解像度化」に基づいています。また、シーン内のすべての曇りガラスマテリアルは、不透明オブジェクトレンダリング後に一回だけ生成されるぼかしテクスチャを合成するだけになりましたので、オブジェクトが多数存在する場合のパフォーマンス低下も小さくできるかと思います。

    キャンセル

+1

 軽量化について

文字数の限界につき別回答ですがご容赦ください。
下記のようなアプローチを試してみました。

  • シェーダーはぼかし処理を担当するFrostBlurと、ぼかし画像合成を担当するFrostCompositionに分ける。
    FrostCompositionは旧Frostを改造、FrostBlurはイメージエフェクトシェーダーに近い作りになっている。
  • カメラが不透明オブジェクト・背景の描画を終え、透明オブジェクトに取りかかる前の位置にコマンドバッファを挿入し、ここでぼかし処理を行う。
  • 画面全体をレンダーテクスチャにコピーしぼかしをかける。
    ぼかしカーネルは一次元9点で固定、重みもシェーダー内に直書きし、これを縦横二段掛けとして高速化を狙った。
  • ぼかし幅が固定されている代わりに、ぼかし量の異なる多段階の画像を作っておくことで可変ぼかしに対応できるようにする。
    ぼかし後の画像を縦横半分にし、再びぼかしをかける操作を繰り返し、解像度が半々に小さくなるぼかし画像群を得る。
  • 曇りガラスオブジェクトの描画の際には、各ぼかしレベルから採った色をBlurに応じて混合する。ノイズテクスチャによるサンプリング位置ずらしは旧Frostと同様に行う。

コマンドバッファ挿入用スクリプト
これを曇りガラスオブジェクトにアタッチした上で、さらにマテリアルはFrostCompositionを使用する。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteInEditMode]
public class Frost : MonoBehaviour
{
    private const CameraEvent FrostBlurEvent = CameraEvent.AfterImageEffectsOpaque;
    private static Material blurMaterial;
    private static readonly Dictionary<Camera, CameraInfo> Cameras = new Dictionary<Camera, CameraInfo>();
    private static readonly int GrabTex = Shader.PropertyToID("_GrabTex");
    private static readonly int[] FrostTex =
    {
        Shader.PropertyToID("_FrostTex0"),
        Shader.PropertyToID("_FrostTex1"),
        Shader.PropertyToID("_FrostTex2"),
        Shader.PropertyToID("_FrostTex3")
    };
    private static readonly int[] FrostTexT =
    {
        Shader.PropertyToID("_FrostTex0T"),
        Shader.PropertyToID("_FrostTex1T"),
        Shader.PropertyToID("_FrostTex2T"),
        Shader.PropertyToID("_FrostTex3T")
    };

    private static void AddCommandBufferIfNeeded(Camera cam)
    {
        if (Cameras.ContainsKey(cam))
        {
            return;
        }

        var cb = new CommandBuffer
        {
            name = "FrostBlur"
        };
        cam.AddCommandBuffer(FrostBlurEvent, cb);
        Cameras.Add(cam, new CameraInfo(cb, 0, 0));
    }

    private static void CleanCameras()
    {
        foreach (var pair in Cameras)
        {
            if (pair.Key != null)
            {
                pair.Key.RemoveCommandBuffer(FrostBlurEvent, pair.Value.CommandBuffer);
            }
        }

        Cameras.Clear();
    }

    private void OnEnable()
    {
        CleanCameras();
    }

    private void OnDisable()
    {
        CleanCameras();
    }

    private void OnWillRenderObject()
    {
        if (!this.enabled || !this.gameObject.activeInHierarchy)
        {
            CleanCameras();
            return;
        }
        var cam = Camera.current;
        if (cam == null)
        {
            return;
        }
        AddCommandBufferIfNeeded(cam);
        var camInfo = Cameras[cam];
        var width = cam.pixelWidth;
        var height = cam.pixelHeight;
        if ((width == camInfo.Width) && (height == camInfo.Height))
        {
            return;
        }
        var cb = camInfo.CommandBuffer;
        Cameras[cam] = new CameraInfo(cb, width, height);
        var blur = blurMaterial;
        if (blur == null)
        {
            blur = new Material(Shader.Find("Hidden/FrostBlur"));
            blurMaterial = blur;
        }

        // 必要に応じ(画面サイズが変わった場合)コマンドバッファを再構成する
        cb.Clear();
        // 一時テクスチャを取得
        // _GrabTexは画面と同サイズ、_FrostTex0~4と_FrostTex0T~4Tは
        // 0が画面と同サイズで、以降はサイズを半々に小さくする
        cb.GetTemporaryRT(GrabTex, width, height, 0, FilterMode.Bilinear);
        for (int i = 0, w = width, h = height; i < 4; i++, w >>= 1, h >>= 1)
        {
            cb.GetTemporaryRT(FrostTex[i], w, h, 0, FilterMode.Bilinear);
            cb.GetTemporaryRT(FrostTexT[i], w, h, 0, FilterMode.Bilinear);
        }
        // 現在のレンダリング結果を_GrabTexにコピー
        cb.Blit(BuiltinRenderTextureType.CurrentActive, GrabTex);
        // _FrostTex0に_GrabTexをぼかしたものを格納
        cb.Blit(GrabTex, FrostTex[0]); // そのままコピー
        cb.Blit(FrostTex[0], FrostTexT[0], blur, 0); // 水平ぼかしコピー
        cb.Blit(FrostTexT[0], FrostTex[0], blur, 1); // 垂直ぼかしコピー
        // 段階的に縮小テクスチャへコピーしつつ、さらにぼかしをかけていく
        for (var i = 1; i < 4; i++)
        {
            cb.Blit(FrostTex[i - 1], FrostTex[i]); // 縮小コピー
            cb.Blit(FrostTex[i], FrostTexT[i], blur, 0); // 水平ぼかしコピー
            cb.Blit(FrostTexT[i], FrostTex[i], blur, 1); // 垂直ぼかしコピー
        }
        // Blitなどのコストを無視すれば、ぼかしテクスチャ総面積は画面面積×1.33で
        // サンプリング回数はピクセルあたり9×縦横2回で18回、さらに後でグラブテクスチャと
        // 各ぼかしテクスチャから1回ずつサンプリングするので合計23回となり、
        // 総サンプリング回数はおよそ画面面積×30.5回となるはず...
        // 当初の方式でのσ=8クラスのぼかし(およそ描画面積×2600回のサンプリングが必要)と
        // 比べると、同等のぼかし量ながらだいぶ処理コストを削減できたのではないでしょうか?
    }

    private struct CameraInfo
    {
        public readonly CommandBuffer CommandBuffer;
        public readonly int Width;
        public readonly int Height;

        public CameraInfo(CommandBuffer cb, int w, int h)
        {
            this.CommandBuffer = cb;
            this.Width = w;
            this.Height = h;
        }
    }
}

ぼかし処理担当シェーダー
上記Frostスクリプトが内部で使用する。

Shader "Hidden/FrostBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        CGINCLUDE
        #include "UnityCG.cginc"
        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };
        struct v2f
        {
            float2 uv : TEXCOORD0;
            float4 vertex : SV_POSITION;
        };
        v2f vert (appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = v.uv;
            return o;
        }
        sampler2D _MainTex;
        float4 _MainTex_TexelSize;
        // 二項係数に基づく重み
        #define WS 256.0
        #define W0 (70.0 / WS)
        #define W1 (56.0 / WS)
        #define W2 (28.0 / WS)
        #define W3 (8.0 / WS)
        #define W4 (1.0 / WS)
        ENDCG

        // パス0...水平ぼかし
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            fixed4 frag (v2f i) : SV_Target
            {
                float2 scale = _MainTex_TexelSize.xy;
                fixed4 col = W0 * tex2D(_MainTex, i.uv);
                col += W1 * tex2D(_MainTex, i.uv + scale * float2(1, 0));
                col += W1 * tex2D(_MainTex, i.uv + scale * float2(-1, 0));
                col += W2 * tex2D(_MainTex, i.uv + scale * float2(2, 0));
                col += W2 * tex2D(_MainTex, i.uv + scale * float2(-2, 0));
                col += W3 * tex2D(_MainTex, i.uv + scale * float2(3, 0));
                col += W3 * tex2D(_MainTex, i.uv + scale * float2(-3, 0));
                col += W4 * tex2D(_MainTex, i.uv + scale * float2(4, 0));
                col += W4 * tex2D(_MainTex, i.uv + scale * float2(-4, 0));
                return col;
            }
            ENDCG
        }

        // パス1...垂直ぼかし
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            fixed4 frag (v2f i) : SV_Target
            {
                float2 scale = _MainTex_TexelSize.xy;
                fixed4 col = W0 * tex2D(_MainTex, i.uv);
                col += W1 * tex2D(_MainTex, i.uv + scale * float2(0, 1));
                col += W1 * tex2D(_MainTex, i.uv + scale * float2(0, -1));
                col += W2 * tex2D(_MainTex, i.uv + scale * float2(0, 2));
                col += W2 * tex2D(_MainTex, i.uv + scale * float2(0, -2));
                col += W3 * tex2D(_MainTex, i.uv + scale * float2(0, 3));
                col += W3 * tex2D(_MainTex, i.uv + scale * float2(0, -3));
                col += W4 * tex2D(_MainTex, i.uv + scale * float2(0, 4));
                col += W4 * tex2D(_MainTex, i.uv + scale * float2(0, -4));
                return col;
            }
            ENDCG
        }
    }
}

ぼかしテクスチャ合成担当シェーダー
旧Frostと同様に、曇りガラスオブジェクトにはこれを使ったマテリアルをセットする。

Shader "Custom/FrostComposition"
{
    Properties
    {
        _Color("Color", Color) = (1, 1, 1, 1)

        _MainTex("Diffuse", 2D) = "white" {}
        _Noise("Noise", 2D) = "black" {}

        _Range("Range", Float) = 0.025
        _Blur("Blur", Range(0.0, 1.0)) = 0.5
    }

    SubShader
    {
        Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

        Cull Off

        CGINCLUDE
        #include "UnityCG.cginc"
        half4 _Color;
        sampler2D _MainTex;
        float4 _MainTex_ST;
        sampler2D _GrabTex;
        sampler2D _FrostTex0;
        sampler2D _FrostTex1;
        sampler2D _FrostTex2;
        sampler2D _FrostTex3;
        sampler2D _Noise;
        float4 _Noise_ST;
        half _Range;
        float _Blur;
        ENDCG

        Pass
        {
            CGPROGRAM
            #pragma target 3.0
            #pragma vertex vert
            #pragma fragment frag

            struct v2f
            {
                float4 pos : SV_POSITION;
                float3 uv : TEXCOORD;
                float4 screenPos : TEXCOORD1;
            };

            v2f vert(appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                o.screenPos = ComputeGrabScreenPos(o.pos);
                return o;
            }

            half4 frag(v2f i) : SV_Target
            {
                float2 uv = i.screenPos.xy / i.screenPos.w;
                float2 frostUV = tex2D(_Noise, i.uv * _Noise_ST.xy + _Noise_ST.zw).xy;

                frostUV -= 0.5;
                frostUV *= _Range;
                frostUV += uv;
                // frostUV = uv;

                float t = pow(_Blur, 0.5) * 4;
                float4 frost = smoothstep(1, 0, t) * tex2D(_GrabTex, frostUV);
                frost += smoothstep(1, 0, abs(t - 1)) * tex2D(_FrostTex0, frostUV);
                frost += smoothstep(1, 0, abs(t - 2)) * tex2D(_FrostTex1, frostUV);
                frost += smoothstep(1, 0, abs(t - 3)) * tex2D(_FrostTex2, frostUV);
                frost += smoothstep(1, 0, 4 - t) * tex2D(_FrostTex3, frostUV);

                half4 diffuse = tex2D(_MainTex, i.uv * _MainTex_ST.xy + _MainTex_ST.zw);

                half alpha = _Color.a * diffuse.a;

                return half4(frost.xyz + (diffuse.rgb * _Color.rgb * alpha), 1);
            }
            ENDCG
        }
    }
    Fallback Off
}

ぼかし画像を作る過程は下図のようになります。GIFでも階調を表現できるよう、白黒で撮影しました。

ぼかし過程

FrostCompositionでは、Blurに応じて下図のような比率で各ぼかし画像を混合しました。

ぼかし混合比率

旧Frostと比較するとプロファイラーで見た際のレンダリング負荷(黄緑色領域)が削減されましたが、新Frostは低解像度のぼかし画像を引き伸ばしたものですので、よく見るとピクセル由来のブロック感が出ています。
σに応じて広い範囲をきっちりサンプリングしていた旧Frostに対し、新Frostは複数のぼかし画像を混ぜているだけですので、画質面では劣ってしまいます。一応ガウス分布っぽい広がりのぼかしにはしたつもりですが、「ガウシアンブラー」だと言い張るにはちょっと怪しいかもしれません...
ノイズ効果と組み合わせれば、粗が目立たなくなってマシなように思います。

新旧Frostの比較
プロファイラー

_Blurとσの対応

対応グラフ1
対応グラフ2

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/10/10 13:02

    返信が遅れてしまい申し訳ありません。
    長文での詳しいご回答本当にありがとうございます。
    試してみたところこちらでも動作の高速化が確認できました。
    こんな書き方もあるのかと驚いております。
    質問なのですが一応これもガウシアンブラーの手法にのっとった方法なのでしょうか。またBlurの値とσの値を対応づけはできるのでしょうか。
    質問ばかりで申し訳ありません。

    キャンセル

  • 2018/10/10 22:29 編集

    痛いところを突いた鋭いご質問だと思います...
    新Frostを作っているときは「σとの対応は大して考慮しなくても、Blurスライダーの量におおむね比例した見た目でぼかしがかかればいいだろう」などと考えていました。
    ガウシアンブラーだと言ってもいいのは、個々のぼかし画像を作っている横ぼかし・縦ぼかし部分までかもしれません。縮小によるぼけ成分や、複数ぼかし画像合成による可変ぼかしもどきは、正統なガウシアンブラーからの誤差を生む要因になるはずです。

    ですが、ある程度はσとBlurの対応付けも可能かと思います。
    ぼかしの重みは二項係数に基づいていて、カーネルサイズ2n+1=9、つまりn =4ですので分散は2*4*0.5*0.5=2、よって標準偏差√2ですので、σ=√2...約1.4のガウシアンブラーと同程度の結果が得られると考えられます。
    また、半分の解像度でガウシアンブラーを行うことは、サンプリング範囲が縦横2倍に広がったようなものですので、分散を4倍...σを2倍にするようなものと言えるでしょう。
    そして、σaのガウシアンブラーがかかった画像にさらにσbのガウシアンブラーをかけると、分散が2つのブラーの和になりますので、σ=√(σa^2 + σb^2)のガウシアンブラーを1回かけるのと同等の効果が得られます。
    ですので、各ぼかしレベルのσは...

    _GrabTex...ぼかしなし、極めて0に近い微小値 ≒ 0
    _FrostTex0...√2 ≒ 1.41
    _FrostTex1...√(8+2) = √10 ≒ 3.16
    _FrostTex2...√(32+8+2) = √42 ≒ 6.48
    _FrostTex3...√(128+32+8+2) = √170 ≒ 13.0

    と見積もられます。これらをsmoothstepで合成して、Blurとの散布図としてプロットしたものを回答に追記しました。ご覧の通りぐにゃぐにゃしているものの、わりと正比例していますので...

    _Sigma ≒ 13 * _Blur
    _Blur ≒ _Sigma / 13

    で、だいたいの換算ができそうです。

    一応近似多項式も図に入れていますが(_Blur→_Sigmaは10次近似でけっこうフィットしましたが、_Sigma→_Blurは次数を上げてもいまいちフィットしなかったので、5次までとしました)、そもそも_Sigmaの方は推定値ですし、あまり計算コストをかける必要はないのではないかと思います。

    キャンセル

  • 2018/10/11 12:59

    なるほど。確かに見た限り誤差の範囲内ですし、_Sigmaと_Blurの対応付けも可能で、これだけパフォーマンスが向上するのならお釣りがくるぐらいですね。
    改めて詳しい説明ありがとうございました。勉強にもなりましたしとても助かりました。
    コメントでの不躾な質問にも答えてくださり、感激しております。
    shaderも勉強していきたいと思います。本当にありがとうございました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.59%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る