【Unity】ベジェ曲線を学び、実装する
ベジェ曲線とは
数式で曲線を表現する方法の一つです。
コンピュータ上で滑らかな曲線を表現できるため、多くのドローソフトや文字の描画で採用されています。
ベジェ曲線の原理
ベジェ曲線を理解するには、1次ベジェ曲線から一つずつ次元を上げながら動きを見ていくのが分かりやすいと思います。
1次ベジェ曲線
*1
一本の線分(P0 から P1)の上を一定の割合で動く点を考えます。この点が動く軌跡が1次ベジェ曲線です。
つまり、ただの直線です。しかし、この動きがすべての基本になります。
「t」は線分上をどれだけの割合進んだのかを表す数値です。
2次ベジェ曲線
*2
2次ベジェ曲線は、線分が一本増えて二本の線分(P0 から P1 と、P1 から P2)から成り立ちます。
1次ベジェ曲線と同じように、それぞれの線分上を一定の割合で動く点(緑色の点)を考えます。
その点同士を結ぶと、なめらかに動く一本の線分(緑色の線分)が出来上がります。
さらに、緑色の線分上を一定の割合で動く点(黒い点)をまた考えることができます。この点が動く軌跡が2次ベジェ曲線です。
3次ベジェ曲線
*3
3次ベジェ曲線はさらに線分が一本増えて、三本の線分(P0 から P1、P1 から P2、P2 から P3)から成り立ちます。
1次ベジェ曲線と同じように、それぞれの線分上を一定の割合で動く点(緑色の点)を考えます。
その点同士を結ぶと、なめらかに動く二本の線分(緑色の線分)が出来上がります。
二本の緑色の線分に注目します。それぞれの線分上を一定の割合で動く点(青色の点)をまた考えることができます。
その点同士を結ぶと、なめらかに動く一本の線分(青色の線分)が出来上がります。
青色の線分に注目します。青色の線分上を一定の割合で動く点(黒い点)をまた考えることができます。この点が動く軌跡が3次ベジェ曲線です。
実装
今回は3次ベジェ曲線を実装しました。
UnityEngine.Vector3 には、ベクトルの線形補間を行う Lerp メソッドが用意されています。
Lerp メソッドを使って素直に実装すると下記のようになります。
Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { var a = Vector3.Lerp(p0, p1, t); // 緑色の点1 var b = Vector3.Lerp(p1, p2, t); // 緑色の点2 var c = Vector3.Lerp(p2, p3, t); // 緑色の点3 var d = Vector3.Lerp(a, b, t); // 青色の点1 var e = Vector3.Lerp(b, c, t); // 青色の点2 return Vector3.Lerp(d, e, t); // 黒色の点 }
これでも動きますが、数学的には下記のように最適化できます。
Vector3 GetPoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) { var oneMinusT = 1f - t; return oneMinusT * oneMinusT * oneMinusT * p0 + 3f * oneMinusT * oneMinusT * t * p1 + 3f * oneMinusT * t * t * p2 + t * t * t * p3; }
この関数を使って、Illustratorのペンツール風(?)に曲線を描いていけるGUIを実装しました。
github.com
Unity で Illustrator のペンツール風にベジェ曲線が描けるやつ。コーナーポイント/スムーズポイントの切り替えとか、アンカーの削除ができる。ここから塗りつぶしとかいろいろ実装してみたくなる。#unity3d https://t.co/PjwLhtfttL pic.twitter.com/VkXPi3BUbS
— setchi (@setchi) April 9, 2017
まだ最低限の機能のみですが、機会を見つけたら実装練習も兼ねてリッチにしていきます。
参考
- 作者: 久富木隆一
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/09/24
- メディア: Kindle版
- この商品を含むブログを見る
【Unity】「FancyScrollView」を公開しました
高度に柔軟なアニメーションを実装できる汎用のScrollViewコンポーネントです。 無限スクロールも対応しています。
github.com
以下ほぼ README のコピペです
導入
Unity 2017.1.0 (C# 6.0) 以降が必要です。このリポジトリを Clone するか、 Asset Store からプロジェクトにインポートしてください。
サンプル
FancyScrollView/Examples/Scenes/ を参照してください。
サンプル名 | 説明 |
01_Basic | 最もシンプルな構成の実装例です。 |
02_CellEventHandling | セルからのイベントをハンドリングする実装例です。 |
03_InfiniteScroll | 無限スクロールの実装例です。 |
04_FocusOn | ボタンで左右のセルにフォーカスする実装例です。 |
仕組み
FancyScrollView はセルの位置を更新する際に、画面に見える範囲を正規化した値を各セルに渡します。セル側では 0.0 ~ 1.0 の値をもとにスクロール中の見た目を自由に制御できます。
使い方
もっともシンプルな構成では、
- セルにデータを渡すためのオブジェクト
- セル
- スクロールビュー
の実装が必要です。
スクリプトの実装
セルにデータを渡すためのオブジェクトを定義します。
public class ItemData { public string Message; }
FancyScrollViewCell を継承して自分のセルを実装します。
using UnityEngine; using UnityEngine.UI; using FancyScrollView; public class MyScrollViewCell : FancyScrollViewCell<ItemData> { [SerializeField] Text message; public override void UpdateContent(ItemData itemData) { message.text = itemData.Message; } public override void UpdatePosition(float position) { // position は 0.0 ~ 1.0 の値です // position に基づいてスクロールの外観を自由に制御できます } }
FancyScrollView を継承して自分のスクロールビューを実装します。
using UnityEngine; using System.Linq; using FancyScrollView; public class MyScrollView : FancyScrollView<ItemData> { [SerializeField] Scroller scroller; [SerializeField] GameObject cellPrefab; protected override GameObject CellPrefab => cellPrefab; void Start() { scroller.OnValueChanged(base.UpdatePosition); } public void UpdateData(IList<ItemData> items) { base.UpdateContents(items); scroller.SetTotalCount(items.Count); } }
スクロールビューにデータを流し込みます。
using UnityEngine; using System.Linq; public class EntryPoint : MonoBehaviour { [SerializeField] MyScrollView myScrollView; void Start() { var items = Enumerable.Range(0, 50) .Select(i => new ItemData {Message = $"Cell {i}"}) .ToArray(); myScrollView.UpdateData(items); } }
インスペクタ上の設定
My Scroll View
プロパティ | 説明 |
---|---|
Cell Spacing | セル同士の間隔を float.Epsilon ~ 1.0 の間で指定します。 |
Scroll Offset | スクロールのオフセットを指定します。例えば 0.5 を指定してスクロール位置が 0 の場合、最初のセルの位置が 0.5 になります。 |
Loop | オンにすると、セルをループして配置します。無限スクロールさせたい場合はオンにします。 |
Cell Prefab | セルの Prefab を指定します。 |
Cell Container | セルの親要素となる Transform を指定します。 |
Scroller
プロパティ | 説明 |
---|---|
Viewport | ビューポートとなる RectTransform を指定します。ここで指定された RectTransform の範囲内でジェスチャーの検出を行います。 |
Direction Of Recognize | ジェスチャーを認識する方向を Vertical か Horizontal で指定します。 |
Movement Type | コンテンツがスクロール範囲を越えて移動するときに使用する挙動を指定します。 |
Scroll Sensitivity | スクロールの感度を指定します。 |
Inertia | 慣性のオン/オフを指定します。 |
Deceleration Rate | Inertia がオンの場合のみ有効です。減速率を指定します。 |
Snap - Enable | Snap を有効にする場合オンにします。 |
Snap - Velocity Threshold | Snap がはじまる閾値となる速度を指定します。 |
Snap - Duration | Snap 時の移動時間を秒数で指定します。 |
Data Count | アイテムのデータ件数の総数です。基本的にスクリプトから設定します。 |
Q&A
データ件数が多くてもパフォーマンスは大丈夫?
表示に必要なセル数のみが生成されるため、データ件数がパフォーマンスに与える影響はわずかです。セル間のスペース(同時に存在するセルの数)とセルの演出は、データ件数よりもパフォーマンスに大きな影響を与えます。
自分でスクロール位置を制御したいんだけど?
FancyScrollView の次の API を使用してスクロール位置を更新できます。
protected void UpdatePosition(float position)
サンプルで使われている Scroller を使う場合は、次の API を使用して FancyScrollView のスクロール位置を更新できます。
public void ScrollTo(int index, float duration)
public void JumpTo(int index)
public void OnValueChanged(Action<float> callback)
Scroller を使わずにあなた自身の実装で全く違った振る舞いをさせることもできます。
セルで発生したイベントを受け取れる?
セル内で発生したあらゆるイベントをハンドリングできます。 サンプルにセルで発生したイベントをハンドリングする実装が含まれていますので、参考にして実装してください。
セルをLoop(無限スクロール)させたいんだけど?
無限スクロールをサポートしています。実装手順は下記の通りです。
- ScrollView の Loop をオンにするとセルが循環し、最初のセルの前に最後のセル、最後のセルの後に最初のセルが並ぶようになります。
- サンプルで使用されている Scroller を使う場合は、「Movement Type」を「Unrestricted」にするとスクロール範囲が無制限になります。
実装例( FancyScrollView/Assets/FancyScrollView/Examples/03_InfiniteScroll at master · setchi/FancyScrollView · GitHub
)が含まれていますので、こちらも参考にしてください。
その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡いただくか、プルリクをお願いします。
【Unity】「HIT」のログイン時のようなエフェクトをつくる
HITとは?
NEXONが出してるスマホゲームです。UE4製で綺麗なグラフィックのアクションゲームです。
mobile.nexon.co.jp
ログイン時のエフェクトってどんなの?
実装方針
まずこんな画像を用意します。
こういうのはパーリンノイズが向いてる気がするので、パーリンノイズで生成しました。
色を高さと見なして、こんな風にしたらそれっぽいのができそうですね!
(図は 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); } }
ここまでの結果です。
仕上げ
フチに色を付けたり、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) ); }
まとめ
マスク画像を工夫すればもっと面白い表現ができそうです。
できればフラグメントシェーダ内の条件分岐をなくしたいです。
2017/01/26追記:シェーダーの改善
今回の記事のような処理は、αカットアウトシェーダを使うとよりシンプルに実装できると指摘をいただきました。
docs.unity3d.com
Unityのビルトインシェーダにあるαカットアウトシェーダ(Unlit/Transparent Cutout)を使って実装してみます。
仕組み
αカットアウトシェーダは名前の通り、画像のα値と閾値によってピクセルを破棄するか決定します。
元画像のα成分にマスク情報を埋め込んだ画像を用意します。
この画像を、Unlit/Transparent Cutout シェーダをセットしたマテリアルで描画して、_Cutoff プロパティを操作するとこのように動きます。
(前回と背景が変わっていますが、分かりやすさのためです。結果は前回とほぼ同じです)
フチに色を付ける
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
実行結果です。
最初の実装と比較して
最初の実装では画像を二枚使用していたのに対し、一枚の画像で実装できました。
また、不要なピクセルをより手前(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 = @"https?://(?:[!-~]+\.)+[!-~]+"; const string RegexHashtag = @"[##][A-Za-zA-Za-z一-鿆0-90-9ぁ-ヶヲ-゚ー]+"; void Start() { text.OnClick(RegexURL, Color.cyan, url => Debug.Log(url)); text.OnClick(RegexHashtag, Color.green, hashtag => Debug.Log(hashtag)); } }
結果
おわりに
現状、Canvas の Render Mode が Screen Space - Overlay の場合にしか対応できていません。今後対応予定です。
2016/08/22 追記: すべての Render Mode に対応しました。
その他、不具合・要望があれば
setchi (@setchi) | Twitter
に連絡ください!
【Unity】シェーダーを利用して音声波形を描く
今、広い範囲の音声波形を高速にリアルタイム描画する問題に取り組んでいます。
要件として描画対象の範囲をグリグリ変更できる必要があって、これまで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です。見やすく縦方向に伸ばしています)
シェーダーで波形を描画する
フルソースはここにあります。
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 ); }
最終的にこのように出力されます。
【Unity C#】Undo / Redoの実装
今Unityで音ゲーの譜面を作るエディタを開発していて、Undo/Redo の実装をする機会があったのでメモです。
今回は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
【n Back Tracer】リリースしました。
Unity習作2本目のカジュアルゲームをリリースしました。
次々と現れるパターンを記憶しながら、N個前のパターンを素早くなぞる脳トレゲームです。難しい選択肢を選ぶほど高得点を狙えます。
n Back Tracer - Google Play の Android アプリ
コードはGitHubに公開しています。
setchi/n_back_tracer · GitHub
【Kagaribi】リリースしました
Unity習作のカジュアルゲームをリリースしました。
火の玉を操作して白い枠をできるだけ多くくぐっていくシンプルなゲームです。
真ん中を通るのが高得点のコツです。
Kagaribi - Google Play の Android アプリ
コードはGitHubに公開しています。
setchi/kagaribi · GitHub