LoginSignup
4
8

More than 1 year has passed since last update.

【Unity】ブラーで疾走感のあるポストエフェクトを作る

Posted at

概要

こういうエフェクトを作りました。
画面端のみに残像が生じるようになっているので、レースゲーム等で疾走感を出しつつプレイする上で重要となる画面中心の視認性を保つことが出来ます。

GIF

pillar_blur.gif

real_blur.gif

静止画

image.png

ソースコード

シェーダ

MotionBlur.shader
Shader "Hidden/MotionBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BlurSize ("Blur Size", Float) = 0
        _EdgeCoeff ("Edge Coefficient", Float) = 1
        _SpeedCoeff ("Speed Coefficient", Float) = 0
        _BlurCenterPoint ("Blur Center Point", Vector) = (0.5, 0.5, 0.0)
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #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;
            half _BlurSize;
            half _EdgeCoeff; // 端の方だけに効果をかけるために使用する係数。大きいほど端のみに効果が表れる
            half _SpeedCoeff; // スピードに応じて増減させる係数(0~1)。大きくするほど効果が強くなる。ゲーム側の最高速度で1に、停止中は0にする。
            half2 _BlurCenterPoint; // ブラーの中心となる点

            static const int BLUR_SAMPLE_COUNT = 8;
            // 近い点から遠い点に向かってサンプリングする際の重みづけ係数を設定していく
            // 総和が1になるようにする
            static const float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT,
                1.0 / BLUR_SAMPLE_COUNT
            };

            float magnitude(float2 vec){
                return max(abs(vec.x), abs(vec.y));
            }

            // 画面上におけるブラーの中心点までの最大距離を計算する
            float calcMaxDistance()
            {
                // どの点が中心点だったとしても、最大距離を取るのは四角のどれか
                float distance1 = magnitude(float2(0, 0) - _BlurCenterPoint);
                float distance2 = magnitude(float2(1, 0) - _BlurCenterPoint);
                float distance3 = magnitude(float2(0, 1) - _BlurCenterPoint);
                float distance4 = magnitude(float2(1, 1) - _BlurCenterPoint);

                float maxDistance = max(distance1, max(distance2, max(distance3, distance4)));

                return maxDistance;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = 0;
                
                // ブラーの中心から該当ピクセルまでの方向ベクトル。このベクトルに沿ってサンプリングを行う
                float2 dir = i.uv - _BlurCenterPoint;

                // ブラーの中心からの距離。距離が遠いほど強くブラーがかかるようにする。
                // ここでアスペクト比を考慮すれば画面上下端に左右端と同じだけの効果を与えることが出来るかもしれない(今回は純粋に距離だけを見たいのでそうしない)
                float distance = magnitude(dir);

                // 方向ベクトルを正規化
                dir /= sqrt(dir.x * dir.x + dir.y * dir.y);

                // 画面の中心から最も遠い点までの距離が1になるように距離を正規化
                distance /= calcMaxDistance(); 

                distance = pow(distance, _EdgeCoeff); // distanceは0~1の範囲を取るので、累乗することで、より端の方だけを効果の対象にする事が出来る。

                for(int j = 0; j < BLUR_SAMPLE_COUNT; j++){
                    float2 samplePoint = i.uv - dir / BLUR_SAMPLE_COUNT * j * distance * _SpeedCoeff *_BlurSize;
                    col += tex2D(_MainTex, samplePoint) * BLUR_WEIGHTS[j] ;
                }

                return col;
            }
            ENDCG
        }
    }
}


C#スクリプト

MotionBlur.cs
using UnityEngine;

public class MotionBlur : MonoBehaviour
{
    [SerializeField] Material motionBlurMaterial;
    [SerializeField] float speed;

    private void Update()
    {
        // 十字キーの上下 or W,Sキーで前後に移動する処理
        float vertical = Input.GetAxis("Vertical");
        transform.position += Vector3.forward * vertical * speed * Time.deltaTime;

        motionBlurMaterial.SetFloat("_SpeedCoeff", Mathf.Abs(vertical));
    }

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Graphics.Blit(src, dest, motionBlurMaterial);
    }
}

リポジトリ

こちらのリポジトリにソースコードと、サンプルシーンを配置しています。Unityプロジェクトではないので、別途作成したプロジェクトにインポートしてください。

解説

シェーダコードの解説をします。

パラメータ

まず使用するプロパティですが、以下の5つです。

sampler2D _MainTex;
half _BlurSize;
half _EdgeCoeff; // 端の方だけに効果をかけるために使用する係数。大きいほど端のみに効果が表れる
half _SpeedCoeff; // スピードに応じて増減させる係数(0~1)。大きくするほど効果が強くなる。ゲーム側の最高速度で1に、停止中は0にする。
half2 _BlurCenterPoint; // ブラーの中心となる点

_MainTexにはエフェクト適用前のレンダリング結果が自動的に渡されます。
_BlurSizeは変数名の通り、ブラーによって生じる残像の大きさを表しています。大きくすると残像が大きくなります。
_EdgeCoeffは残像が生じる範囲に影響を与えるパラメータで、値が小さいほど広い範囲に残像を生じさせます。
_SpeedCoeffはスクリプトから渡されるパラメータで、現在の速さが最高速度の何割程度なのかを0~1の範囲で設定します。
_BlurCenterPointは残像が生じる中心点を操作することが出来るようになっており、残像が生じる範囲と残像の方向が影響を受けます。下の画像は、画面下端中央を中心点に設定した際の様子です。残像が上方向に延びている事が分かるかと思います。
image.png

定数として以下の2つを使用しています。

static const int BLUR_SAMPLE_COUNT = 8;
// 近い点から遠い点に向かってサンプリングする際の重みづけ係数を設定していく
// 総和が1になるようにする
static const float BLUR_WEIGHTS[BLUR_SAMPLE_COUNT] = {
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT,
    1.0 / BLUR_SAMPLE_COUNT
};

BLUR_SAMPLE_COUNTはその名の通り、ブラーをかけるために、いくつの点から色を取得してきて合成するかを表す数です。細かくすればするほど負荷が上がりますが残像のクオリティが上がります。
BLUR_WEIGHTSは、各サンプリングポイントから取ってくる色にかける係数の配列です。0番が今エフェクトをかけているピクセルの色にかける係数で、7番がそのピクセルから最も遠いサンプリング点の色にかける係数です。デフォルトでは全て等しくしていますが、遠いサンプリング点ほど値を小さくする等工夫すれば綺麗な残像を作ることが出来るかもしれません。全ての係数の総和が1になるようにしなければ画面が全体的に明るくなったり暗くなったりします。

フラグメントシェーダ

フラグメントシェーダの中では、まず初めにブラーの中心から該当ピクセルまでの距離と方向を計算します。

float2 scale = _BlurSize;
fixed4 col = 0;

// ブラーの中心から該当ピクセルまでの方向ベクトル。このベクトルに沿ってサンプリングを行う
float2 dir = i.uv - _BlurCenterPoint;

// ブラーの中心からの距離。距離が遠いほど強くブラーがかかるようにする。
// ここでアスペクト比を考慮すれば画面上下端に左右端と同じだけの効果を与えることが出来るかもしれない(今回は純粋に距離だけを見たいのでそうしない)
float distance = magnitude(dir);

// 方向ベクトルを正規化
dir /= sqrt(dir.x * dir.x + dir.y * dir.y);

// 画面の中心から最も遠い点までの距離が1になるように距離を正規化
distance /= calcMaxDistance(); 

distance = pow(distance, _EdgeCoeff); // distanceは0~1の範囲を取るので、累乗することで、より端の方だけを効果の対象にする事が出来る。

方向ベクトルdirは後ほどサンプリングする点を決定するために使用します。
距離distanceは後ほどサンプリングする距離に掛け合わせることでdistanceが大きいほど遠い点からサンプリングする=ブラーが強くなるようにします。ここで、distanceはブラーの中心とそこから最も遠い点の間の距離が1となるように正規化をします。
calcMaxDistance関数の中身は以下のようになっています。

float calcMaxDistance()
{
    // どの点が中心点だったとしても、最大距離を取るのは四角のどれか
    float distance1 = magnitude(float2(0, 0) - _BlurCenterPoint);
    float distance2 = magnitude(float2(1, 0) - _BlurCenterPoint);
    float distance3 = magnitude(float2(0, 1) - _BlurCenterPoint);
    float distance4 = magnitude(float2(1, 1) - _BlurCenterPoint);

    float maxDistance = max(distance1, max(distance2, max(distance3, distance4)));

    return maxDistance;
}

中心点がどこにあったとしても、中心点から最も遠い点は四角のいずれかのはずなので、中心点から各角までの距離を計算し、その中で最大のものを返しています。ここで、magnitudeはベクトルの大きさを計算する関数です。

float magnitude(float2 vec){
    return max(abs(vec.x), abs(vec.y));
}

ここでは、L2ノルムではなくL1ノルムを使用しています。L2ノルムを使用すると、残像が歪んでしまうためです。

  • L2ノルム
    norm2.png
  • L1ノルム
    norm1.png

好みの問題だと思いますが、L2ノルムだと角に近いほど残像が強くなるため、カーブを描くような残像が生じます。L1ノルムの残像の方が自然な気がします。ただし、超高速で移動するような演出をしたい場合はL2ノルムの方がかっこよかったりするかもしれません。

中心点からの距離と方向が計算出来たら、サンプリングを行いブラーをかけます。

for(int j = 0; j < BLUR_SAMPLE_COUNT; j++){
    float2 samplePoint = i.uv - dir / BLUR_SAMPLE_COUNT * j * distance * _SpeedCoeff *_BlurSize;
    col += tex2D(_MainTex, samplePoint) * BLUR_WEIGHTS[j] ;
}

まずは、中心点に向かう方向ベクトル-dirBLUR_SAMPLE_COUNTの数だけ分割し、jをかけます。つまり、-dirの方向にあるj番目の点を表しています。ここに、中心からの距離、現在のスピード、ブラーの大きさを掛け合わせて、サンプリングする点を決めます。本来残像は中心に向かって発生するため、サンプリングするべきは中心から離れるdir方向にある点のはずです(外側にある点の色が中心に向かって滲んでくるため)。ここで中心に向かう向きの-dirを使用しているのは、中心から離れる方向にサンプリングを行うと、画面端にアーティファクトが生じてしまうためです。下のGIF画像を見ると、左端の座席が吸い込まれるようになっているのが分かると思います。これは、画面外の点をサンプリングしてしまうために発生する現象です。なので、画面中央に向かってサンプリングする事でその発生を防ぐことが出来ます。残像の向きについても、そもそもサンプリングの方向に依らず両側に色が滲むことから違和感は正直あまり無いと思います。
artifact2.gif

まとめ

普段シェーダコードはあまり書かないので何か無駄なことをやっていたりもっといいやり方があれば是非教えてください。

4
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8