Webブラウザ上でリアルタイムにぼかしを加えたい

 

自己紹介

初めましての人は初めまして、そうでない人はこんばんは。HTMLファイ部のポロです。

この記事はなに?

アドベントカレンダー9日目の記事です。

この記事ではWebブラウザ上でリアルタイムにぼかしを加えるにはどんな方法があるかを書きます。

(当初の予定では「運の上げかた」という自己啓発書に乗ってそうなスピリチュアルな題目で書くつもりでしたが、 書くべき内容が多すぎて纏まらなかったのでまたの機会に書きます)

やりたいこと

ぼかしから徐々にピントが合っていく様子をリアルタイムに表現したい

f:id:s_gozaru:20161210001148p:plainf:id:s_gozaru:20161210001150p:plain

まず思ったこと

SVGやCSSのfilter処理で瞬殺できそう 😏

しかし実際は

Microsoft Edgeくんに謎の設定項目があるため、
ユーザに設定弄ってもらわないとFilterが効かない。

→OMG!

本題

Edgeや古いブラウザでも動く上手いぼかしの方法がないか、いくつかの手法を試していきました

  • stackblur
  • text-shadow🌚
  • canvas拡大🔎
  • 画像クロスフェード🌅

片っ端から紹介していきます。

(順番から察してもらえると思いますが、結果的に画像クロスフェード🌅が一番実用的でした)

stackblur

デモを見た感じ、早いしいい感じにもボケるなと思って使ってみました。

f:id:s_gozaru:20161209234636p:plain

しかし使ってみたところ、ぼかしというより滲んだ感じになってしまいました。

  • 利点:軽くて速い
  • 弱点:滲んだ感じになり、妙な色が紛れる。

text-shadow🌚

CSSのtextshadowの影だけを取り出す技です。

本体を画面外に追いやり、影をだけを取り出して使います。

text-shadow: 9999px 0 10px #fff;
transform: translateX(-9999px);

みたいな感じに使います。

  • 利点:何も準備せずにほぼcssだけで完結する、ちゃんとgaussianなボケかたをする
  • 弱点:テキストしかぼかせない

canvas拡大🔎

小さいキャンバスに描画してキャンバスを拡大する技です。

  • 利点:軽い、簡単
  • 弱点:ぼかし半径を大きくするほど、情報源の画素が減るので、荒っぽい感じになる

ぼかし半径を大きくするとかなり荒っぽい感じになるので、canvas拡大を何パターンか行い半透明で重ねると少しマシになります。しかしそれでも荒っぽさが目立つのでぼかし半径が大きいと弱いです。

imagedataをごりごりする🐗

canvasのimagedataを気合でごりごり操作してgaussianぼかしを再現する技

まじめにgaussianぼかしするライブラリはいくつかあるので、それを使うと素早く試せます

  • 利点:ちゃんと望んだ表現ができる
  • 欠点:処理がむちゃくちゃ重い。

画像クロスフェード🌅

ぼけた画像とぼけてない画像をクロスフェードする技

f:id:s_gozaru:20161210001148p:plainf:id:s_gozaru:20161210001150p:plain

  • 利点:原理的に最初と最後は理想と一致してる
  • 欠点:通信量が増える。

結局ここに落ち着いたという感じがあります。

軽さ・表現力ともに非常に良い感じでした。

 

ただ2枚でクロスフェードすると

f:id:s_gozaru:20161210001711p:plain 

のように途中ではっきりと輪郭が見えてしまうので、

つなぎになる画像をもう一枚くらい足すと良い塩梅になります。

 

 

最後に

今回検証を行うにあたり、様々な方にアドバイスいただきました。感謝!

Unityでよく使うデザインパターン

はじめに

この記事はカヤックUnityアドベントカレンダー2016の9日目の記事になります。
昨日に引き続き清水がお送りします。

今日はUnityの開発でよく使われるデザインパターンをいくつか紹介します。

Flyweight

Flyweightは複数のオブジェクトから共通のオブジェクトを参照することでリソースの節約を図るというデザインパターンです。

例えば、同じ見た目のグラフィックを表示する場合、複数のComponentがそれぞれ個別にMaterialを生成して持つのでなく、1つのMaterialを共有して参照した方がメモリの節約になります。
共有しているオブジェクトを変更すると、それを参照しているオブジェクト全てに影響を与えるので、各インスタンスで特殊化が必要なものには使えないことに注意します。
前述のMaterialの例のように、アセットに関しては自然とこのパターンを使っていることが多いです。

アセット以外の、ランタイムで生成するオブジェクトや、GameObjectが保持しているオブジェクトに関してはFlyweightを意識して設計した方がよい場合があります。
例えば、昨日の記事でも例として上げた、1つのprefabから複数のオブジェクトをインスタンス化する場合を考えてみましょう。
prefabに含まれるMonoBehaviourの中に、巨大なオブジェクトをシリアライズして保持するフィールドがあったとします。
そのprefabをInstantiateすると、保持している巨大なオブジェクトがまるまる複製されるので、CPUのコストも、複製を保持するためのメモリのコストも大きなものとなります。
各インスタンスでその巨大なオブジェクトを変更しないなら、そのオブジェクトをScriptableObjectに分離することでこの問題を解決できます。

以下のコードはパターン適用前のものです。

using System;
using UnityEngine;

public class VeryFatObject : MonoBehaviour
{
    // とても要素数が多い配列
    public string[] data;
}

public class NoFlyweightSample
{
    private VeryFatObject _prefab;

    private Start()
    {
        for (int i = 0; i < 10; i++)
        {
            // VeryFatObject.data がまるまる複製されるため、インスタンス化に時間がかかる
            // VeryFatObject.data 10個分のメモリが必要になる
            Instantiate(_prefab)
        }
    }
}

ScriptbleObjectに分離してパフォーマンスの向上を図ったものが以下のコードです。

using System;
using UnityEngine;

public class VeryFatObject : ScriptableObject
{
    // とても要素数が多い配列
    public string[] data;
}

public class LightObject : MonoBehaviour
{
    // 配列そのものでなく、ScriptableObjectへの参照を持つ
    [SerializeField]
    private VeryFatObject _object;
}

public class FlyweightSample : MonoBehaviour
{
    private LightObject _prefab;

    private void Start()
    {
        for (int i = 0; i < 10; i++)
        {
            Instantiate(_prefab);
        }
    }
}

変更後のコードでは、LightObjectはVeryFatObjectへの参照を持っているだけなのでInstantiateがすぐに終わりますし、メモリ使用量も1つ分のVeryFatObjectで済みます。

ScriptableObjectとして分離するのが難しい場合には、そのオブジェクトのフィールドを空にした状態で複製して、複製後に参照をセットする方法もあります。
複製対象のクラスのソースコードを変更できない場合ではこちらの方法を使うことになるでしょう。

using System;
using UnityEngine;

public class VeryFatObject : MonoBehaviour
{
    // とても要素数が多い配列
    public string[] data;
}

public class RuntimeFlyweightSample
{
    [SerializeField]
    private VeryFatObject _prefab;

    private void Start()
    {
        // dataを一時退避
        string[] tmp = _prefab.data;

        // 複製前にdataを消しておく
        _prefab.data = null;

        for (int i = 0; i < 10; i++)
        {
            // dataが無いのですぐ終わる
            var clone = (VeryFatObject)Instantiate(_prefab);

            // 全てのインスタンスで同じVeryFatObjectへの参照が使われる
            clone.data = tmp;
        }

        // 複製元のdataを戻す
        _prefab.data = tmp;
    }
}

Singleton

Singletonは、あるクラスのインスタンスを1つしか作らないように制限するデザインパターンです。
そのクラスを使う箇所全てでnewして使うと効率が悪いため、1つだけインスタンスを作って使いまわそうというものです。
静的クラスのように使いたいけれど、何らかの理由でインスタンスが必要な場合に使います。
UnityではMonoBehaviourの機能を利用するために使われることが多いです。

using UnityEngine;

public class SingletonSample : MonoBehaviour
{
    private static SingletonSample _instance;

    public static SingletonSample instance
    {
        get
        {
            if (!_instance)
            {
                var go = new GameObject("SingletonSample");
                DontDestroyOnLoad(go);
                _instance = go.AddComponent<SingletonSample>();
            }
            return _instance;
        }
    }

    private void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
        }
        else
        {
            // 本来のSingletonの実装ではコンストラクタをprivateにして外からnewできないようにするが、
            // MonoBehaviourではコンストラクタを定義できない。
            // そのため、すでにインスタンスがあったら破棄する。
            Destroy(this);
        }
    }
}

Singletonは、静的クラスと同様に、密結合が増えたり、ユニットテストがしづらいなどの問題があるので、使いどころには注意しましょう。
絶対に1つのインスタンスしか使わないと確信できる場合にのみ使い、多用は避けます。

Object Pool

Object Poolはオブジェクトが繰り返し生成されたり破棄されたりするのを回避するためのパターンです。
使い終わったオブジェクトを破棄せずにキャッシュして、必要になったら再利用することで、オブジェクトの生成数の合計を同時に使う数だけに抑えることができます。
生成、破棄のコストが重い場合に効果あるので、GameObjectを使い回す目的でよく使われます。
シューティングゲームの弾丸など、大量に使うが1つ1つの寿命が短いオブジェクトが効果的な例として有名です。
注意点として、再利用時に前回使用時の状態が残っているとバグになるため、再利用時、または使用終了時に確実に初期化されるようにします。

サンプルコードを書こうと思いましたが、公式のチュートリアルに記事があったので、そちらを紹介することで代えさせて頂きます。
ObjectPool.csの作成

Object Poolを使用する場合、使い終わったオブジェクトもキャッシュに残ってメモリを余分に使うというコストがあるので、導入するべきかどうかの検討はしっかりしましょう。
キャッシュを使う場合の常として、バグが発生しやすいという懸念もあります。

おわりに

デザインパターンにはいろいろなものがありますが、今回は特にMonoBehaviourと関わりが深いものを紹介させて頂きました。
デザインパターンをいろいろ知っておくと、良い設計や問題解決の助けになるので、他のパターンも勉強してみると良いでしょう。
ただし、適していない対象に使った場合、無駄に処理が複雑化したり、実行時のパフォーマンスが下がったりします。
そのため、パターンが適しているかどうかをよく考えて使うのが重要となります。

明日はアファトによるテクスチャフォーマットの記事になります。