読者です 読者をやめる 読者になる 読者になる

setchi’s blog

コードに埋もれてます。

【Unity】「FancyScrollView」を公開しました

セルの動きを自由に制御できる汎用 ScrollView です。アイディア次第でさまざまな ScrollView を作れます。
github.com

f:id:setchi_q:20170226144546g:plain
f:id:setchi_q:20170226144523g:plain
f:id:setchi_q:20170226144706g:plain

以下ほぼ README のコピペです

仕組み

FancyScrollView はセルの位置を更新する際に、画面に見える範囲を正規化した値を各セルに渡します。セル側では 0.0 ~ 1.0 の値をもとにスクロール中の見た目を自由に制御してください。

使い方

もっともシンプルな構成では、

  • セルにデータを渡すためのオブジェクト
  • スクロールビュー
  • セル

の実装が必要です。

スクリプトの実装

セルにデータを渡すためのオブジェクトを定義します。

public class MyCellDto
{
    public string Message;
}

FancyScrollView を継承して自分のスクロールビューを実装します。

using UnityEngine;
using System.Linq;

public class MyScrollView : FancyScrollView<MyCellDto>
{
    [SerializeField]
    ScrollPositionController scrollPositionController;

    void Awake()
    {
        base.Awake();

        base.cellData = Enumerable.Range(0, 50)
            .Select(i => new MyCellDto { Message = "Cell " + i })
            .ToList();

        scrollPositionController.SetDataCount(base.cellData.Count);
        scrollPositionController.OnUpdatePosition(base.UpdatePosition);
    }
}

FancyScrollViewCell を継承して自分のセルを実装します

using UnityEngine;
using UnityEngine.UI;

public class MyScrollViewCell : FancyScrollViewCell<MyCellDto>
{
    [SerializeField]
    Text message;

    public override void UpdateContent(MyCellDto itemData)
    {
        message.text = itemData.Message;
    }

    public override void UpdatePosition(float position)
    {
        // position は 0.0 ~ 1.0 の値です
        // position をもとに、セルの見た目を自由に制御してください
    }
}

インスペクタ上の設定

f:id:setchi_q:20170226145459p:plain

My Scroll View
プロパティ 説明
Cell Interval セル同士の間隔を float.Epsilon ~ 1.0 の間で指定します。
Cell Offset セルのオフセットを指定します。例えば 0.5 を指定して、スクロール位置が 0 の場合、最初のセルの位置が 0.5 になります。
Loop オンにすると、セルをループして配置します。無限スクロールさせたい場合はオンにします。
Cell Base セルのもととなる GameObject を指定します。
Scroll Position Controller
プロパティ 説明
Viewport ビューポートとなる RectTransform を指定します。ここで指定された RectTransform の範囲内でジェスチャーの検出を行います。
Direction Of Recognize ジェスチャーを認識する方向を Vertical か Horizontal で指定します。
Movement Type Scroll Rect の Movement Type と似た挙動をします。
Elasticity Scroll Rect の Elasticity と似た挙動をします。
Scroll Sensitivity スクロールの感度を指定します。
Inertia 慣性のオン/オフを指定します。
Deceleration Rate Inertia がオンの場合のみ有効です。減速率を指定します。
Snap - Enable Snap を有効にする場合オンにします。
Snap - Velocity Threshold Snap がはじまる閾値となる速度を指定します。
Snap - Duration Snap 時の移動時間を秒数で指定します。
Data Count アイテムのデータ件数の総数です。基本的にスクリプトから指定します。

Q&A

データ件数が多くてもパフォーマンスは大丈夫?

セルは表示に必要な数のみ生成するので、データ件数がパフォーマンスに与える影響は小さいです。 データ件数よりも、セルの間隔(同時に存在するセルの数)やセルの演出の方がパフォーマンスに与える影響が大きいです。

自分でスクロール位置を制御したいんだけど?

スクロール位置は自分で好きなように制御できます。サンプルで使用している ScrollPositionController は独自の実装に置き換えられます。

セルで発生したイベントを受け取れる?

セルで発生したあらゆるイベントをハンドリングできます。 サンプルにセルで発生したイベントをハンドリングする実装を含めているので、それを参考に実装してください。

セルをLoop(無限スクロール)させたいんだけど?

  • ScrollView の「Loop」をオンにすると、セルをループして配置します。
  • サンプルで使用している ScrollPositionController を使う場合は、「Movement Type」を「Unrestricted」にするとスクロール範囲が無制限になります。

f:id:setchi_q:20170226150234p:plain



その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡いただくか、プルリクをお願いします。

【Unity】「HIT」のログイン時のようなエフェクトをつくる

HITとは?

NEXONが出してるスマホゲームです。UE4製で綺麗なグラフィックのアクションゲームです。
mobile.nexon.co.jp

ログイン時のエフェクトってどんなの?

f:id:setchi_q:20170125221344g:plain

実装方針

まずこんな画像を用意します。
こういうのはパーリンノイズが向いてる気がするので、パーリンノイズで生成しました。
f:id:setchi_q:20170124011309p:plain
色を高さと見なして、こんな風にしたらそれっぽいのができそうですね!
f:id:setchi_q:20170124234952g:plain
(図は Processing で生成しました)
EffectVisualize.pde · GitHub

実装

シェーダの全体像です。

Shader "Custom/LoginEffectShader"
{
    Properties
    {
        [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
        _MaskTex("Mask Texture", 2D) = "white" {}
        _Height("Height", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
        }

        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color : COLOR;
                float2 texcoord  : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _MaskTex;
            float _Height;

            fixed4 frag(v2f IN) : SV_Target
            {
                float maskHeight= tex2D(_MaskTex, IN.texcoord).r;
                if (maskHeight> _Height) {
                    discard;
                }
                return tex2D(_MainTex, IN.texcoord);
            }
                ENDCG
        }
    }
}

Properties ブロック内で、ノイズ画像の _MaskTex と現在の高さを表す _Height を定義します。

_MaskTex("Mask Texture", 2D) = "white" {}
_Height("Height", Float) = 0

フラグメントシェーダ内で、今見ている位置より高い位置のピクセルを破棄しています。

fixed4 frag(v2f IN) : SV_Target
{
    float maskHeight = tex2D(_MaskTex, IN.texcoord).r;
    if (maskHeight > _Height) {
        discard;
    }
    return tex2D(_MainTex, IN.texcoord);
}

あとは、C# スクリプトから適当に _Height を操作します。

using DG.Tweening;
using UnityEngine;

public class EffectTweener : MonoBehaviour
{
    [SerializeField]
    SpriteRenderer _spriteRenderer;

    Material _material;
    int _heightPropertyID;
    bool _isHidden = false;

    void Start()
    {
        _heightPropertyID = Shader.PropertyToID("_Height");
        _material = _spriteRenderer.material;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_isHidden)
            {
                Play(from: 0f, to: 1f);
            }
            else
            {
                Play(from: 1f, to: 0f);
            }
            _isHidden = !_isHidden;
        }
    }

    void Play(float from, float to)
    {
        DOTween.Kill(this);
        DOTween.To(
                () => from,
                a => _material.SetFloat(_heightPropertyID, a),
                to,
                1.5f)
            .SetId(this);
    }
}

ここまでの結果です。
f:id:setchi_q:20170125223215g:plain

仕上げ

フチに色を付けたり、Bloomで光らせたりして盛ります。

fixed4 frag(v2f IN) : SV_Target
{
    float maskHeight = tex2D(_MaskTex, IN.texcoord).r;
    if (maskHeight > _Height) {
        discard;
    }
    
    fixed4 color = tex2D(_MainTex, IN.texcoord);
    float edgeHeight = 0.015;
    return lerp(
        color,
        fixed4(0, 4, 2, 0),
        step(_Height - edgeHeight, maskHeight)
    );
}

f:id:setchi_q:20170124014044g:plain

まとめ

マスク画像を工夫すればもっと面白い表現ができそうです。
できればフラグメントシェーダ内の条件分岐をなくしたいです。




2017/01/26追記:シェーダーの改善

今回の記事のような処理は、αカットアウトシェーダを使うとよりシンプルに実装できると指摘をいただきました。
docs.unity3d.com

Unityのビルトインシェーダにあるαカットアウトシェーダ(Unlit/Transparent Cutout)を使って実装してみます。

仕組み

αカットアウトシェーダは名前の通り、画像のα値と閾値によってピクセルを破棄するか決定します。
元画像のα成分にマスク情報を埋め込んだ画像を用意します。
f:id:setchi_q:20170128154149p:plain
この画像を、Unlit/Transparent Cutout シェーダをセットしたマテリアルで描画して、_Cutoff プロパティを操作するとこのように動きます。
f:id:setchi_q:20170128154402g:plain
(前回と背景が変わっていますが、分かりやすさのためです。結果は前回とほぼ同じです)

フチに色を付ける

Unityのビルトインシェーダはここからダウンロードできます。
https://unity3d.com/jp/get-unity/download/archive

フチに色が付くように Unlit/Transparent Cutout の frag 関数内を改造したシェーダをつくりました。

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.texcoord);
    clip(col.a - _Cutoff);
    UNITY_APPLY_FOG(i.fogCoord, col);

    float edgeHeight = 0.015;
    return lerp(
        col,
        fixed4(0, 4, 2, 1),
        step(col.a, _Cutoff + edgeHeight)
    );
}

改造したシェーダの全体像はこちらです。
https://gist.github.com/setchi/b5c9fd72c3cb5317dae44cb6f3eb7fef

実行結果です。
f:id:setchi_q:20170128154424g:plain

最初の実装と比較して

最初の実装では画像を二枚使用していたのに対し、一枚の画像で実装できました。
また、不要なピクセルをより手前(Alpha Test)の段階で破棄できて効率的になりました。

【Unity】uGUIで使えるハイパーテキストを作りました

経緯

以前 uGUI Text で Twitter のクリック可能なハッシュタグのようなものを実装しようとして挫折したのをふと思い出して再挑戦しました。

作ったもの

uGUI Text の指定した部分文字列にクリック時のコールバックや文字色を設定できる仕組みを作りました。
github.com

使い方

Text を継承した抽象クラスの HypertextBase.cs に、任意の位置の文字列に文字色とクリックされたときのコールバックを設定できるAPI を用意しているので、継承して自分の好きなように実装します。
サンプルとして正規表現によるハイパーテキストの実装例を置いてあります。

RegexHypertext.cs

uGUI-Hypertext/RegexHypertext.cs at master · setchi/uGUI-Hypertext · GitHub

こんな風に使えます。

using UnityEngine;

public class RegexExample : MonoBehaviour
{
    [SerializeField]
    RegexHypertext _text;

    const string RegexURL = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
    const string RegexHashtag = "[##][A-Za-zA-Za-z一-鿆0-90-9ぁ-ヶヲ-゚ー]+";

    void Start()
    {
        _text.SetClickableByRegex(RegexURL, Color.cyan, url => Debug.Log(url));
        _text.SetClickableByRegex(RegexHashtag, Color.green, hashtag => Debug.Log(hashtag));
    }
}

結果

f:id:setchi_q:20160731232100g:plain

おわりに

現状、Canvas の Render Mode が Screen Space - Overlay の場合にしか対応できていません。今後対応予定です。
2016/08/22 追記: すべての Render Mode に対応しました。


その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡ください!

【Unity】シェーダーを利用して音声波形を描く

f:id:setchi_q:20151025203335p:plain

今、広い範囲の音声波形を高速にリアルタイム描画する問題に取り組んでいます。

要件として描画対象の範囲をグリグリ変更できる必要があって、これまでLineRendererやGLによる描画を試みましたがどれも欲しいパフォーマンスに届きませんでした。

そこで、波形情報をテクスチャに埋め込んでシェーダーで描画する方法を試してみました。

AudioClipから波形情報を取得する

var audioSource = GetComponent<AudioSource>();
var samples = new float[audioSource.clip.samples * audioSource.clip.channels];
audioSource.clip.GetData(samples, 0);

で取得できます。
docs.unity3d.com

波形情報をテクスチャに埋め込む

描画領域の横幅 x 1サイズのテクスチャを生成して、色情報に波形データを埋め込んでいきます。
手抜き実装なので r 成分しか使ってませんが、rgba 全ての成分を使えばより小さいテクスチャサイズで実現できます。
また、1サンプルを1ピクセルに対応させるとテクスチャサイズが膨大になりすぎるので適当に間引きします。

public class WaveformRenderer : MonoBehaviour
{
    [SerializeField]
    AudioSource audioSource;
    [SerializeField]
    RawImage image;
    [SerializeField]
    int imageWidth;

    Texture2D texture;
    float[] samples = new float[500000];

    void Start()
    {
        texture = new Texture2D(imageWidth, 1);
        texture.SetPixels(Enumerable.Range(0, imageWidth).Select(_ => Color.clear).ToArray());
        texture.Apply();
        image.texture = texture;
    }

    void Update()
    {
        audioSource.clip.GetData(samples, audioSource.timeSamples);

        int textureX = 0;
        int skipSamples = 200;
        float maxSample = 0;

        for (int i = 0, l = samples.Length; i < l && textureX < imageWidth; i++)
        {
            maxSample = Mathf.Max(maxSample, samples[i]);

            if (i % skipSamples == 0)
            {
                texture.SetPixel(textureX, 0, new Color(maxSample, 0, 0));
                maxSample = 0;
                textureX++;
            }
        }

        texture.Apply();
    }
}

説明に余分なコードは省いています。実際のソースコードはここにあります。
NoteEditor/WaveformRenderer.cs at 21a1878556f1b492a7a9a150339512a6ba5330ca · setchi/NoteEditor · GitHub

このようなテクスチャが生成されます。
(実際は縦1pxです。見やすく縦方向に伸ばしています)
f:id:setchi_q:20151025202741p:plain

シェーダーで波形を描画する

フルソースはここにあります。
NoteEditor/Waveform.shader at a56ec2d55987a8f22c392bfe1739af87a9e97bc9 · setchi/NoteEditor · GitHub


重要なのは下記の部分です。
テクスチャの r 成分からボリュームを取り出して、自身のV座標がボリュームの範囲内なら緑、そうでなければ黒を出力しています。

fixed4 frag(v2f v) : SV_Target{
    float volume = tex2D(_MainTex, v.uv.x).r * 0.5;
    float uvY = v.uv.y - 0.5;

    return lerp(
        fixed4(0, 0, 0, 1),
        fixed4(0, 1, 0, 1),
        -volume < uvY && uvY < volume
    );
}

最終的にこのように出力されます。
f:id:setchi_q:20151025201359p:plain

良くなった点

GLで即時描画していた時は必ず毎フレーム更新する必要がありました。
この方法では波形に変化がなければ前に適用したテクスチャを使えるのでCPUの処理を少なくできます。

テクスチャに直接波形を描画するのと比べた場合、Y方向の展開はGPU側で行うのでCPU → GPUへのデータ転送量が削減できます。

【Unity】スクリプトからuGUIのEventTriggerへリスナー登録する拡張を書きました

f:id:setchi_q:20150727222736p:plain
uGUI要素へ特殊なイベントを登録する際EventTriggerコンポーネントを追加してインスペクター上からぽちぽちやっていくと思いますが、スクリプトから直接AddListenerできる拡張を書きました。

こんな風にイベントリスナを登録できます。EventTriggerが無い場合は自動的にAddComponentされます。

// 要 using UnityEngine.EventSystems;
button.AddListener(EventTriggerType.PointerUp, e => Debug.Log("PointerUp!"));

【Unity C#】Undo / Redoの実装

今Unityで音ゲーの譜面を作るエディタを開発していて、Undo/Redo の実装をする機会があったのでメモです。
f:id:setchi_q:20150716001544g:plain

今回はCommandパターンで実装しました。

UndoRedoManager.cs
NoteEditor/CommandManager.cs at master · setchi/NotesEditor · GitHub

 

ユーザが何か行動すると、それに対するUndoメソッド/Redoメソッドを内包したコマンドオブジェクトをマネージャに渡します。
NoteEditor/CanvasWidthScalePresenter.cs at master · setchi/NotesEditor · GitHub

 

マネージャは受け取ったコマンドをUndo用スタックにPushしていきます。
Undoの実行はUndo用スタックからPopして、コマンドのUndoメソッドを実行します。

RedoはUndoに対するUndo実装です。
NoteEditor/CommandManager.cs at master · setchi/NotesEditor · GitHub

【Unity】テクスチャを分割して個別に保存する

f:id:setchi_q:20150308174638p:plain


↑のような画像を等間隔に分割したくて、なんかUnityで動くスクリプト書きました。

まず対象の画像ファイルをAssetsフォルダに放り込みます。
スクリプトから画素値を扱うための設定として、ImportSettingsでTextureTypeを「Advanced」にして、Read/Write Enabledにチェックを入れる必要があります。
f:id:setchi_q:20150308184836p:plain

ImageSlicerスクリプトを適当なゲームオブジェクトにアタッチして、対象のテクスチャを選んで、縦横の分割数と保存先をいれます。
f:id:setchi_q:20150308175428p:plain
再生すると指定したディレクトリに分割された画像が保存されます。
f:id:setchi_q:20150308190104p:plain

(汚い)ソースはgistにあげました。