画面の見栄えを良くするテクニックはいろいろありますが、パッと見の印象が大きく変えたい!となったらやっぱり**Bloom
**が一番効果が高いイメージがあります。
ただ、**「UIを光らせたい」**となったときにCanvasにBloomを反映させるのはけっこう乱暴だと思ってて、現実的じゃないという認識です。
ビジュアルの調整が難しいということもあるんですが、そもそも3Dモデルを映すカメラにはBloom以外にブラーなどのポストエフェクトをかけることもあるだろうから、それを配慮すると 3DカメラとUIカメラのそれぞれでBloomをかけないといけない ということになってしまい、負荷的に超キツい。
ということで、 ポストエフェクト無しでBloomみたいな効果 を出せるか挑戦してみました。
■おまけ
[Unity]uGUIをBloomを使って光らせ、EmissionColorも設定できるようにする
一応普通にBloom使って光らせる方法もまとめました。
こんな感じになりました
必要な機能はできました。
自分のセンスではちょっと良いビジュアルは作れなかったですが・・・必要な機能はできました(強調)
なにをしてるのか
こういう見た目を作るために・・・
ベースのImageと、発光っぽい表示用のImageの2つの画像を重ねています。
光るということはつまり加算合成なので、ぼかしの入った方は加算合成で表示されるようにしています。
この方法で光る機能を提供するために
- UIの加算合成シェーダー
- ぼかし画像の自動生成機能
を作り、それぞれをカジュアルに使えるように整理しました。
順番に説明します。
UIの加算合成シェーダー
UIの標準シェーダーを複製して、加算合成されるようなシェーダーにカスタムします。
まず下記ページから、自身のUnityバージョン&OSのビルドインシェーダーをダウンロードします。
ダウンロードしたファイルを解凍し、UIの標準シェーダーであるUI-Default.shader
を探します。
これをコピーして、ファイル名はUI-Addtive.shader
としてプロジェクトに入れましょう。
UI-Addtive.shaderを編集
ZTest [unity_GUIZTestMode]
Blend One One // ← 加算合成モードにする
ColorMask [_ColorMask]
デフォルトだとBlend One OneMinusSrcAlpha
となっている行を、Blend One One
に変更して、加算合成で下の画像とブレンドされるようにします。
これだけ!
- 参考:Unityで加算合成とかしたい(@tetr4lab
)
新規でUI-Addtive
と名前をつけたマテリアルを作り、このシェーダーを反映しておきましょう。
このマテリアルをUIのマテリアルに設定すると、UIが加算合成表示になります。
また、この後このマテリアルをスクリプトから読み込むため、Resources
フォルダに入れておいてください。
ぼかし画像の自動生成機能
Unityでブラー画像動的生成(@divideby_zero)の記事を参考に(というかほぼそのままパクリ)させていただきました!
ガウシアンフィルタでぼかしを効かせた画像を生成しています。
Imageオブジェクトにアタッチして使用する、ImageFakeBloom.cs
というコンポーネントを作ります。
機能としては
- 子オブジェクトに自動でImageオブジェクトを追加
- 子のImageオブジェクトに自動で
UI-Addtive
マテリアルを付与 - Imageオブジェクトに設定されている画像のぼかし画像を自動生成&子のImageオブジェクトに設定
- エディタモードでも各種設定の反映を確認できる
- 任意でぼかし画像をプロジェクトに追加
というものです。
ImageFakeBloom.cs
using UnityEngine;
using UnityEngine.UI;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
#endif
[ExecuteInEditMode, RequireComponent(typeof(Image))]
public class ImageFakeBloom : MonoBehaviour
{
[SerializeField]
private Image _image;
[SerializeField]
private Image _glowImage;
/// <summary> 発光色 </summary>
[SerializeField]
private Color _glowColor = Color.white;
/// <summary> 上乗せ画像のぼかし距離 </summary>
[SerializeField]
private float _blurSig = 5f;
// ぼかし画像の再生成判定用
private float _preBlurSig;
private Sprite _preOrifinSprite;
/// <summary>
/// 起動時
/// </summary>
private void Awake()
{
UpdateGlow();
}
/// <summary>
/// Inspector上の値が変更されたときに呼び出し
/// </summary>
private void OnValidate()
{
UpdateGlow();
}
/// <summary>
/// 初期化
/// </summary>
private void Initialize()
{
_image = GetComponent<Image>();
// 上乗せする発光表現用のImageの生成
_glowImage = new GameObject("Glow", typeof(Image)).GetComponent<Image>();
// 加算合成マテリアルを設定
_glowImage.material = Resources.Load<Material>("UI-Additive");
_glowImage.transform.SetParent(_image.transform, false);
_glowImage.gameObject.layer = _image.gameObject.layer;
_glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;
_preBlurSig = _blurSig;
_preOrifinSprite = _image.sprite;
}
/// <summary>
/// 発光表現用のぼかし画像の更新
/// </summary>
private void UpdateGlow()
{
if (_image == null)
{
// ベースのImageを取得していなかったら初期化
Initialize();
}
if (_image.sprite == null)
{
// ベースのImageの画像が設定されていなかったら何もしない
_glowImage.sprite = null;
return;
}
_glowImage.color = _glowColor;
if (_glowImage.sprite != null && _preBlurSig == _blurSig && _preOrifinSprite == _image.sprite)
{
// ぼかし距離とベースImageに変更がなければぼかし画像の再生成をしない
return;
}
Sprite preGlowSprite = _glowImage.sprite;
Texture2D blurTex = CreateBlurTexture(_image.sprite.texture, _blurSig);
Sprite blurSprite = Sprite.Create(blurTex, _image.sprite.rect, _image.rectTransform.pivot);
_glowImage.sprite = blurSprite;
_glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;
#if UNITY_EDITOR
// エディターでのみ分かりやすいようにスプライト名をつける
blurSprite.name = _image.sprite.name + " blur";
#endif
// 使わなくなったスプライトを破棄
DestroySprite(preGlowSprite);
_preBlurSig = _blurSig;
_preOrifinSprite = _image.sprite;
}
/// <summary>
/// 更新
/// </summary>
private void Update()
{
_glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;
_glowImage.color = _glowColor;
}
/// <summary>
/// 削除時に呼び出し
/// </summary>
private void OnDestroy()
{
DestroySprite(_glowImage.sprite);
}
/// <summary>
/// スプライトの破棄
/// </summary>
private void DestroySprite(Sprite sprite)
{
if (sprite == null)
{
return;
}
if (Application.isPlaying)
{
Destroy(sprite.texture);
Destroy(sprite);
}
else
{
// エディターモードでは即時破棄
DestroyImmediate(sprite.texture);
DestroyImmediate(sprite);
}
}
/// <summary>
/// ぼかし画像を生成
/// https://qiita.com/divideby_zero/items/4c02177a56f7d500d4c0
/// </summary>
/// <param name="sig">ぼかし距離</param>
private static Texture2D CreateBlurTexture(Texture2D tex, float sig)
{
sig = Mathf.Max(sig, 0f);
int W = tex.width;
int H = tex.height;
int Wm = (int)(Mathf.Ceil(3.0f * sig) * 2 + 1);
int Rm = (Wm - 1) / 2;
//フィルタ
float[] msk = new float[Wm];
sig = 2 * sig * sig;
float div = Mathf.Sqrt(sig * Mathf.PI);
//フィルタの作成
for (int x = 0; x < Wm; x++)
{
int p = (x - Rm) * (x - Rm);
msk[x] = Mathf.Exp(-p / sig) / div;
}
var src = tex.GetPixels(0).Select(x => x.a).ToArray();
var tmp = new float[src.Length];
var dst = new Color[src.Length];
//垂直方向
for (int x = 0; x < W; x++)
{
for (int y = 0; y < H; y++)
{
float sum = 0;
for (int i = 0; i < Wm; i++)
{
int p = y + i - Rm;
if (p < 0 || p >= H) continue;
sum += msk[i] * src[x + p * W];
}
tmp[x + y * W] = sum;
}
}
//水平方向
for (int x = 0; x < W; x++)
{
for (int y = 0; y < H; y++)
{
float sum = 0;
for (int i = 0; i < Wm; i++)
{
int p = x + i - Rm;
if (p < 0 || p >= W) continue;
sum += msk[i] * tmp[p + y * W];
}
dst[x + y * W] = new Color(1, 1, 1, sum);
}
}
var createTexture = new Texture2D(W, H);
createTexture.SetPixels(dst);
createTexture.Apply();
return createTexture;
}
#if UNITY_EDITOR
[CustomEditor(typeof(ImageFakeBloom))]
public class ImageFakeBloomEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
ImageFakeBloom target = this.target as ImageFakeBloom;
if(target._glowImage == null || target._glowImage.sprite == null)
{
return;
}
// 発光表現用に生成された画像をプロジェクトに保存するボタン
if (GUILayout.Button("発光画像を保存"))
{
SaveBlurSprite(target._glowImage.sprite);
}
}
/// <summary>
/// 渡されたSpriteをプロジェクトに保存
/// </summary>
private void SaveBlurSprite(Sprite sprite)
{
string path = EditorUtility.SaveFilePanelInProject("発光画像を保存", sprite.name, "png", "保存する画像名を入力してください");
if (path.Length == 0)
{
// 保存キャンセル
return;
}
byte[] pngData = sprite.texture.EncodeToPNG();
if (pngData == null)
{
Debug.LogError("画像データの取得に失敗しました");
return;
}
File.WriteAllBytes(path, pngData);
AssetDatabase.Refresh();
// テクスチャタイプをSpriteに変更
TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
if (textureImporter != null)
{
textureImporter.textureType = TextureImporterType.Sprite;
textureImporter.SaveAndReimport();
}
}
}
#endif
}
このスクリプトをImageオブジェクトにアタッチして使ってください。
使い方
Glow Color
を変更すると発光色が変化します。
Blur Sig
の値を大きくすると、より広い範囲で発光するような見栄えになります。
発光画像を保存
ボタンで、自動生成されたぼかし画像をプロジェクトに保存することもできます。
※保存していなければ、ぼかし画像は実行時に生成され、オブジェクト破棄時に削除されます
光ってるように見えますかね?どうでしょう
課題
① そのまま使うとドローコールが増えやすい
この仕組みで発光したUIをたくさん配置すると、標準のUIマテリアルとUI-Addtive
マテリアルが交互に並ぶことになり、ドローコールが無闇に増えてしまいます。
また、ぼかし画像を自動生成に頼るとアトラスにも所属されないので、バッチされません。
- 自動生成されたぼかし画像が所属するアトラスを自動的に生成&追加
- ぼかしUIは直接ベースのUIの子に入れず、ぼかしUI用のレイヤー(親オブジェクト)にまとめる
- かつベースのUIのサイズや位置は追従する
ということができれば大量に表示してもドローコールを抑えられるんですが・・・むずかしい
② Imageのトリミングやマスクに対応していない
例えばHPゲージにこのなんちゃってBloomを使うと、発光部分がスパッと切られた表示になって偽の発光であることがバレバレでダサいです。
これは・・・解決できるんだろうか・・・
課題①は特に大きな問題なので、実用化にはまだちょっと時間がかかりそう。
アイデアやもっと良い方法を是非教えてください!
今回はここまで。