- 投稿日:2019-12-09T22:51:27+09:00
【Unity】無料アセット「CSV Serialize」を使ってスプレッドシートでマスタデータ管理
プロトタイプ作成に便利そうなアセット「CSV Serializer」の使い方の紹介
簡単に説明すると。CSVを簡単にC#クラスにパースしてくれるアセット
スプレッドシートもCSV形式でダウンロードすればすごく簡単にUnityに持ってこれる1.データクラスを用意
てきとうにスプレッドシートで管理したいデータクラスを作る
public class Enemy { public int id; public string name; public int hitPoint; public int power; public int defence; }2.対応するスプレッドシートのシートを用意
これをCSV形式でダウンロードできるように公開設定する
・「ファイル」 →「ウェブに公開」
・シートの指定をさっき作ったシートにし、形式を「カンマ区切り (.csv)」にして公開。表示されるURLをコピー3.ダウンロード&デシリアライズ処理を書く
Enemy[] enemyData; IEnumerator DownloadAndDeserialize () { string url = "さっきコピーしたURL"; UnityWebRequest request = UnityWebRequest.Get (url); yield return request.SendWebRequest (); enemyData = CSVSerializer.Deserialize<Enemy> (request.downloadHandler.text); }データクラスをScriptableObjectとかにしておけば開発中はスプレッドシート、製品版はアプリ内に埋め込み、とかも簡単にできる。便利!
- 投稿日:2019-12-09T22:44:29+09:00
Unityでスクリプトのビルドが走らなくなった時はAuto Refreshを確認すること
ある日突然、スクリプトを保存してもビルドされなくなった
いつものようにスクリプトを編集、保存、Unityエディタに切り替えると走るはずの自動ビルドが走らない。
編集したスクリプトをProjectでクリックしてInspectorを確認しても、古いまま。
仕方ないので右クリックしてReimportをするとビルドが走る。不便なので色々なキーワード、「Unity」「ビルド」「自動」「Import」などで検索してみても、まぁ大体Jenkinsとかの記事が出てくるばかりで。。
数日不便なまま過ごした挙句、結局詳しい方に教えていただいた。
原因はAuto Refreshのチェックボックス
原因は、PreferencesのGeneralの先頭にある、Auto Refresh のチェックボックスが外れていた事。
まったく覚えはないけれど、多分Preferencesにフォーカスを移そうとした時に、クリックしてチェックを外してしまったのだと思われる。
同じような事態に陥ってしまった人のために、、、記録として残しておきます。
参考
- 投稿日:2019-12-09T21:34:59+09:00
Quest Basicsの歩き方 その1
みなさん、Oculus Quest向けコンテンツ開発、してますか!
前回、こんなのがあるよ~と軽く紹介だけしたQuest Basics、今回はその中身を紹介します。中身は結構いろいろあるので、何回かに分けて紹介していきます。それどこにあるの、ってあなたのために
ここです。以下のGithubのURLからダウンロードしてみてください。おっと、Starをクリックしていいねするのを忘れずに!
https://github.com/hassydhw/QuestBasic今日紹介するのは、シーン名が _01_xx というシリーズです
この01で始まるシーン群は、Oculus Integrationにある各種のコントローラーの3Dモデルがはいったシーンです。中身のコンテンツは正直なにもないのですが、こんなコントローラーを表示してシーンを構築したい、と思ったときにベースとして使えるシーン群だと思っていただければ。
_01_1は、
初代 Oculus Touchのコントローラーを使ったシーン。見た目がクエストのコントローラーと少し違ってて、コントローラを握ると丸い輪っかが下の方に向いているのでプレイすると少し違和感がありますが、ボタンを押したりスティックを操作するとそれにあわせたアニメーションがついてます。
_01_2は、
手のモデルを使ったシーン。ボタン操作にあわせて指が動きます。このシーンの目の前にあるCubeを掴んで動かすこともできます。
Cubeについているコンポーネントをみてみると・・・
なるほど、RigidbodyとOVRGrabbableスクリプトをつけてやるだけで(実質コード一行も書かなくても)手でつかめるんです!私がすごいんじゃなくてOculusがすごいんだけど。_01_3は、
Oculus Questのコントローラーを使ったシーン。これだと握っているコントローラーと見た目が同じになるので違和感がなくなります。でも残念ながら、こいつにはボタンを押したときのアニメーションがついてないんだよねえ・・・
_01_4は、
左右のコントローラーの場所に細長くしたCubeをつけたシーン。もしBeat Saberのようなゲームを作りたくなったら、このシーンをベースにしていただければ。サンプルだからそっけないCube、というかただの棒だけど、アセットストアでかっちょいい剣のモデルを買って、このCubeの角度や大きさにあわせて配置してくれれば、なんかそれっぽくなると思いませんか?
でも、実はそれだけではないんです。
これらのシーンに配置されているOVRCamerarigは、すでにコントローラー付きでプレファブにしてあるので、それをそのままご自分のシーンにいれて使い回すことができます。
OVRCameraRigWithController
OVRCameraRigWithQuestControllerしかも!
そのプレファブには、すでにOVRPhysicRaycasterスクリプトがついているんです。
このあたりの話は次回もう少し詳しくやるつもりなのですが、このスクリプトは、コントローラーからレーザーポインターをだしてゲームオブジェクトを操作したいときに必要なやつ。つまりこのプレファブは、単にコントローラーが付いているという見た目だけでなく、機能的にも、このままで便利に使える優れもの。
ぜひ自分の心向けに使ってください。今日はここまで
今日はシーン名が01_xx というシリーズの紹介をさせていただきました。非常に基本的なシーンだけど、ここで使っているプレファブが便利設定になっているので使いまわしてほしい、というのも伝えたかったことです。実際、このサンプルの02_以降のシリーズはこれらのプレファブをつかってできています。
ところで、あなた誰?、って方
宣伝っぽくて恐縮ですが、VRプロフェッショナルアカデミーという、VRコンテンツの作り方を教える学校で、初心者向けコースの講師をやっています。このBasicsも、生徒向けの教材の一環として作ったものなのですが、自分の生徒以外にも初心者の方には広く使っていただきたいとおもっています。
VRコンテンツ作ってみたい!っておもった方はこちらも覗いてみてください。
https://vracademy.jp/vrBeginner.html
- 投稿日:2019-12-09T18:38:19+09:00
遺伝的アルゴリズム的なものを作ってみたお話
はじめに
最近AI/アルゴリズムの講演を聞きました。そこで、遺伝的アルゴリズムに興味を持ったので「作ってみたい!」と思い、作成したので、何かの参考になればと思いご紹介します。
実際の挙動
![]()
これらは毎フレームランダムに動かしているだけですが、とても人工知能ぽくみえます。
実践
ソースコードはこちら
※3D空間上でXZ平面を走っているのを上から撮影していますcar.csfloat rotVelo = Random.Range(-rotateSpeed, rotateSpeed); transform.eulerAngles = transform.eulerAngles + new Vector3(0, rotVelo, 0); float posVelo = Random.Range(0, moveSpeed); transform.position += transform.forward * posVelo;このObjectを
20体用意して、
100mごとに障害物を置きます。
移動データの記録
それから動いた記録を保存するデータを作成します。
data.cs[Serializable] public struct MoveData { public float pos; public float rot; public MoveData(float _pos, float _rot) { pos = _pos; rot = _rot; } }これのリストを作ってランダムに動いた結果を保存しておきます。
具体的な保存の値はそれぞれ、上記のrotVelo
とposVelo
です。全員が壁にぶつかったら集計をします。
その際にどのObjectが一番進んだかを確かめて次の世代を生成します。
遺伝子の受け継ぎ
marry.csfor (int i = 0; i < 一,二番優秀移動データリスト.Count; i++) { bool l = Random.Range(0,2) == 0; 次世代.移動データリスト.Add(l ? 一番優秀.移動データリスト[i] : 二番優秀.移動データリスト[i]); }こうすることで二人の親の遺伝子(移動データ)を持った子供が生まれます。
parent1+parent2->child1
parent1+parent3->child2
parent2+parent3->child3
parent2+parent4->child4
parent3+parent4->child5
parent3+parent5->child6と子供を作っていきます
child7は一番優秀のデータをそのまま受け継ぎます。(最高記録が下がらないように)
chaild8,9,10はランダムに選んだ二人の遺伝子を組み合わせます。ですがそこで突然変異を起こします。
突然変異
一定の確率で値をおかしくしてほしいので、
totuzennhenni.csif(突然変異する) { for (int i = 0; i < 突然変異.移動データリスト.Count; i++) { 突然変異.移動データリスト.Add(l ? 一番優秀.移動データリスト[i] : 二番優秀.移動データリスト[i]); foreach(var 移動データ in 突然変異.移動データリスト) { if(Random.Range(0 , 突然変異でどれくらい変わるか) != 0) 移動データ.pos *= Random.Range(0,2); 移動データ.rot *= Random.Range(-2,2); } } }こんな感じで突然変異させます。
これらの出来上がったデータをObjectに記録させて、
第2世代が始まります。第2世代
第2世代は遺伝で受け継いだデータのとおりに動きます。
データがなくなったらランダムに動き始めます。これらを繰り返して行くとこんな感じになりました。
終わりに
講演とかYoutubeの動画を見ながらこんな感じで作るのかなーって作りきりました。
いいお勉強になって楽しかった。
- 投稿日:2019-12-09T14:48:01+09:00
長いTextの終わりに "..." をつけ、かつ改行させない方法
Textの幅が枠内に収まりきらないとき、私たちのチームでは、以下の記事の
(4) 余談
の部分を参考に、溢れない箇所でカットして、...
を末尾に付与しています。[Unity] Textコンポーネントでテキストを切り捨てずに全文表示させる方法
以下に一部引用させていただきます。
public static void SetTextWithEllipsis(this Text textComponent, string value) { // create generator with value and current Rect var generator = new TextGenerator(); var rectTransform = textComponent.GetComponent<RectTransform>(); var settings = textComponent.GetGenerationSettings(rectTransform.rect.size); generator.Populate(value, settings); // trncate visible value and add ellipsis var characterCountVisible = generator.characterCountVisible; var updatedText = value; if (value.Length > characterCountVisible) { updatedText = value.Substring(0, characterCountVisible - 3); updatedText += "..."; } // update text textComponent.text = updatedText; }しかし、あるとき問題が発覚しました。長い文字列に半角スペースが含まれている場合、
そのスペースで改行されてしまい、↑のコードはその1行目の終わりで可視文字列の終わりと判定されて、右にまだスペースがあるのに早々に...
が打たれて残りが全てカットされてしまうのです。(↓の中身は、「あああああ\nあああああ\nあああああ」のようになっていますが、1行目の最後の文字がリミットと判断されるようです)かといって、TextのHorizontal OverflowをOverflowにすると、今度は全て表示可能とみなされ、
...
を付与されず全部そのまま出てしまいます…。(ただし、この問題は英語のみでは起きないようです。)
そこで、私たちがとった対策は、半角スペースをnbsp(non-breaking space)に置き換えることです。
このnbsp、htmlなどでは
としてご存知の方もいるかと思いますが、文字コードとしても存在します(\u00a0
)。
改行を誘発する半角スペースをnbspに置き換えてしまうことで、改行を抑制するわけです。
上記のコードを書き換えるなら、以下のようになります。public static void SetTextWithEllipsis(this Text textComponent, string value, bool spaceToNbsp) { // create generator with value and current Rect var generator = new TextGenerator(); var rectTransform = textComponent.GetComponent<RectTransform>(); var settings = textComponent.GetGenerationSettings(rectTransform.rect.size); if (spaceToNbsp) { value = value.Replace(" ", "\u00a0"); } generator.Populate(value, settings); // trncate visible value and add ellipsis var characterCountVisible = generator.characterCountVisible; var updatedText = value; if (value.Length > characterCountVisible) { updatedText = value.Substring(0, characterCountVisible - 3); updatedText += "..."; } // update text textComponent.text = updatedText; }やってることとしては単純で、
Populate()
の前に文字列をReplace()
しているだけです。
これで、半角スペースが含まれていても単語で区切らずに...
を付与することができました。英語では問題が起きないところを見るとUnityの処理の不具合のような気もしますが、ともかくこれで想定どおりの挙動になりました。
- 投稿日:2019-12-09T12:22:47+09:00
Unity2019.2から起動するエディタでコード補完(オートコンプリート)を有効にする
状況
UnityのAssetsから直接
.cs
ファイルを開いた際、エディタ上でオートコンプリートが効かず、ものすごく困りました。
MonoBehavior
クラスが認識されず、VSCode/VisualStudio2019の両方でオートコンプリートが無効。なんでや。環境
以下の通りです。
Unity
2019.2.13fVSCode
10.40.2VisualStudio2019
Communityのちょっと前のやつ解決方法
VisualStudioの場合
【Unity】Visual Studioのインテリセンス(自動補完や候補予測)が効かない場合の対処法
UnityでVisualStudio2017のIntelliSenseが動作しなくなる問題を解決する
リンク先の通りにやったらできた!圧倒的感謝。
自分の環境の場合、VS2019のバージョンを16.4.0に上げただけでいけた。
VSCodeの場合
今日からUnity + Visual Studio Codeを用いた快適な開発生活(随時更新中)
こちらに記載のあるアセットを入れてみましたが、解決に至らず。
MonoBehaviour Snippetsという拡張機能を追加してみたら、それっぽくはなりました。
相変わらずMonoBehavior
クラスは認識されないので、根本的な解決にはなってない。解決でき次第、追記したい。。
- 投稿日:2019-12-09T11:47:18+09:00
【Unity】inspectorでクラスを設定する。
- 投稿日:2019-12-09T11:45:41+09:00
Unityで3Dモデルを画像書き出しする
はじめに
アドカレ10日目の記事になります。
3Dゲーム開発を行なっていると、3Dモデルを使ったバナー作成などをクリエイターさんが扱う事があります。
今回はそういった素材用の画像書き出し処理をエンジニアがサポートできるような事を書いていきます。環境
Unityバージョン
Unity: 2018.4.13f1
Scene構成
3D(モデル)の表示をRenderTextureで2D(UI)上に表示しています。
3Dカメラ表示
2Dカメラ表示
やりたい事
画像書き出し方法
Unityからの画像書き出し方法ですが、ここではスクリプト(RenderTextureの写し)を使用した場合と
UnityRecorder
を使用する場合の2パターンを紹介します。1.スクリプトで画像書き出し
c#Capture.csusing System.IO; using UnityEngine; public class Capture : MonoBehaviour { /// <summary> /// 3D用カメラ /// </summary> [SerializeField] private Camera _3dCamera; /// <summary> /// 非アクティブにしたい対象(床) /// </summary> [SerializeField] private GameObject _3dObject; /// <summary> /// 書き出したい背景カラー /// </summary> [SerializeField] private Color _backgroundColor; public Texture2D GetCapture() { // 後から戻せるように背景色を退避 var bgColor = _3dCamera.backgroundColor; // 背景変更する _3dCamera.backgroundColor = _backgroundColor; // いらないオブジェクトを非表示に _3dObject.SetActive(false); // 再描画する _3dCamera.Render(); // RenderTextureを取得する var targetRt = _3dCamera.targetTexture; var width = targetRt.width; var height = targetRt.height; // 現在使用している描画データを退避 var currentRt = RenderTexture.active; // 一時的に使用するRenderTextureを生成 var workRt = RenderTexture.GetTemporary(width, height, 0); // 描画対象にセット RenderTexture.active = workRt; // RenderTextureに描画する { // 背景透過用にclearで塗りつぶす(描画対象範囲が同じなら不要) GL.Clear(true, true, Color.clear); Graphics.Blit(targetRt, workRt); } // 透過書き出しするのでAlphaも含める var format = TextureFormat.ARGB32; // Texture2Dに書き出す var resultTexture = new Texture2D(width, height, format, false); resultTexture.hideFlags = HideFlags.DontSave; resultTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0, false); resultTexture.Apply(); // 退避していたRenderTextureを戻す RenderTexture.active = currentRt; // 生成したRenderTextureを破棄 RenderTexture.ReleaseTemporary(workRt); // 表示を戻す _3dCamera.backgroundColor = bgColor; _3dObject.SetActive(true); return resultTexture; } }流れとしては、
1. 現在の表示設定を退避させる
2. 表示設定を変更し、一時的なRenderTextureを生成
3. 生成したRenderTextureにGLで描画
4. RenderTextureの内容をTexture2Dに写す
5. RenderTextureを破棄し、表示設定を戻す
ということを行なっています。あとはここにファイル書き出しの処理を追加して、ButtonのOnClickアクションと繋げれば
BackgroundColor
に設定したカラー背景で画像書き出しが出来ますCapture.cs/// <summary> /// 画像の保存フォルダ名 /// </summary> private const string SAVE_FOLDER = "CaptureImages"; /// <summary> /// uGUIのButtonから呼ぶ /// </summary> public void SaveCapture() { var tex = GetCapture(); // 保存する画像名(同じ名前だと上書きされるので、複数取るなら連番にする) SaveImage("capture", tex); } /// <summary> /// Textureを画像書き出し /// </summary> private void SaveImage(string fileName, Texture2D tex) { if (!Directory.Exists(SAVE_FOLDER)) { // Assetsと同階層(プロジェクトフォルダ直下)に作る Directory.CreateDirectory(SAVE_FOLDER); } var path = Path.Combine(SAVE_FOLDER, fileName); // 透過画像を扱うので、pngで保存 File.WriteAllBytes($"{path}.png", tex.EncodeToPNG()); }書き出される画像のサイズはTexture2Dのサイズそのままなので、この処理の場合はカメラに設定してあるRenderTextureのサイズで書き出されます。
ちなみに、RenderTextureからTexture2Dへ写す処理は、Unity2018.2からAsyncGPUReadbackを使うことでも書き出す事ができるようです。
UniTaskを使用して実装した例が以下private async UniTask<Texture2D> GetCaptureAsync() { var bgColor = _3dCamera.backgroundColor; _3dCamera.backgroundColor = _backgroundColor; _3dObject.SetActive(false); _3dCamera.Render(); var targetTexture = _3dCamera.targetTexture; var width = targetTexture.width; var height = targetTexture.height; var currentRt = RenderTexture.active; var workRt = RenderTexture.GetTemporary(width, height, 0); RenderTexture.active = workRt; // RenderTextureに描画する { // 背景透過用にclearで塗りつぶす(描画対象範囲が同じなら不要) GL.Clear(true, true, Color.clear); Graphics.Blit(targetRt, workRt); } // == ここまで同じ処理 == var request = AsyncGPUReadback.Request(workRt); RenderTexture.active = currentRt; RenderTexture.ReleaseTemporary(workRt); _3dCamera.backgroundColor = bgColor; _3dObject.SetActive(true); // 非同期待機 await UniTask.WaitUntil(() => request.done || request.hasError); if (request.hasError) { return null; } var format = TextureFormat.ARGB32; // Texture2Dに書き出す var resultTexture = new Texture2D(width, height, format, false); resultTexture.hideFlags = HideFlags.DontSave; var buffer = request.GetData<Color32>(); resultTexture.SetPixels32(buffer.ToArray()); resultTexture.Apply(); return resultTexture; }非同期処理の方が画像生成時に止まる・カクツクといった事が起こりにくいのでこちらの方が良さそうですが、Editorでしか試してないので実機は分かりません。
参考サイト様
・http://edom18.hateblo.jp/entry/2019/02/21/103056
・https://qiita.com/UnagiHuman/items/583219cb0366b758a7fe2.Unity Recorderを使用して画像書き出し
Editorを使用できる場合、Unity2018からPackage ManagerにあるUnity Recorderを使用することで上記画像を簡単に保存する事ができます。(Install方法についてははぶきます)
手順
Add New Recorders
からImage Sequence
を選択します。Capture
の対象をGame View
からRnder Texture Asset
に変更します。RenderTexture
に3Dカメラで設定しているRenderTextureを設定します。Format
をJPEG
からPNG
に変更します(Capture Alphaにチェックを入れなくても透過書き出しできた)。- Game Sceneを実行し3DカメラのSolid Colorの値を書き出したい背景色に変更する。
START RECORDING
で指定したPathに保存されます。おまけ(スクリプト)
右下にロゴを追加する
RenderTextureに描画した後、ロゴを上書き描画する
Capture.cs[SerializeField] private Texture2D _logo; [SerializeField] private float _logoScale; // ~~RenderTexture処理内~~ Graphics.Blit(targetRt, workRt); // ロゴが設定されていればロゴを描画する if (_logo != null) { GL.PushMatrix(); GL.LoadPixelMatrix(-width * 0.5f, width * 0.5f, height * 0.5f, -height * 0.5f); // 右下に描画 var rect = new Rect( width * 0.5f - _logo.width * _logoScale, height * 0.5f - _logo.height * _logoScale, _logo.width * _logoScale, _logo.height * _logoScale ); Graphics.DrawTexture(rect, _logo); GL.PopMatrix(); } // ~~RenderTexture処理内~~画像を縮小する
パターン1. RenderTextureのサイズを小さくする
パターン2. 画像を縮小して再生成するCapture.cs/// <summary> /// 縮小するか /// </summary> [SerializeField] private bool _isThumbnail; /// <summary> /// 縮小サイズ /// </summary> [SerializeField] private float _thumbnailScale; /// <summary> /// uGUIのButtonから呼ぶ /// </summary> public void SaveCapture() { var tex = GetCapture(); // 完成画像を縮小指定なら縮小サイズにする if (_isThumbnail) { tex = CreateThumbnailTexture(tex, (int) (tex.width * _thumbnailScale), (int) (tex.height * _thumbnailScale)); } SaveImage("capture", tex); } /// <summary> /// 縮小画像を作成する /// </summary> public Texture2D CreateThumbnailTexture(Texture2D origin, int thumbWidth, int thumbHeight, Material mat = null) { var currentRt = RenderTexture.active; // 縮小サイズでRenderTextureを作成 var workRt = RenderTexture.GetTemporary(thumbWidth, thumbHeight, 0); RenderTexture.active = workRt; { GL.Clear(true, true, Color.clear); Graphics.Blit(origin, workRt); } // 透過させないならRGB24で var fullTex = new Texture2D(thumbWidth, thumbHeight, TextureFormat.RGB24, false); fullTex.hideFlags = HideFlags.DontSave; fullTex.ReadPixels(new Rect(0, 0, thumbWidth, thumbHeight), 0, 0, false); fullTex.Apply(); RenderTexture.active = currentRt; RenderTexture.ReleaseTemporary(workRt); return fullTex; }トリミングする
Capture.cs// ~~GL処理後~~ Vector2Int resultSize = new Vector2Int(width, height); Vector2 trimStartPosition = new Vector2(); if (_isTrim) { resultSize = _trimSize; trimStartPosition = _trimStartPosition; } // 透過書き出しするのでAlphaも含める var format = TextureFormat.ARGB32; // Texture2Dに書き出す var resultTexture = new Texture2D(resultSize.x, resultSize.y, format, false); resultTexture.hideFlags = HideFlags.DontSave; resultTexture.ReadPixels(new Rect(trimStartPosition.x, trimStartPosition.y, width, height), 0, 0, false); resultTexture.Apply();最終コード
Capture.csusing System.IO; using UniRx.Async; using UnityEngine; using UnityEngine.Rendering; public class Capture : MonoBehaviour { /// <summary> /// 画像の保存フォルダ /// Assetsと同階層に作られる /// </summary> private const string SAVE_FOLDER = "CaptureImages"; /// <summary> /// 3D用カメラ /// </summary> [SerializeField] private Camera _3dCamera; /// <summary> /// 非アクティブにしたい対象(床) /// </summary> [SerializeField] private GameObject _3dObject; /// <summary> /// 書き出したい背景カラー /// </summary> [SerializeField] private Color _backgroundColor; /// <summary> /// 右下に追加するロゴ /// </summary> [SerializeField] private Texture2D _logo; /// <summary> /// ロゴのスケール /// </summary> [SerializeField] private float _logoScale; /// <summary> /// 縮小するか /// </summary> [SerializeField] private bool _isThumbnail; /// <summary> /// 縮小サイズ /// </summary> [SerializeField] private float _thumbnailScale; /// <summary> /// トリミングするか /// </summary> [SerializeField] private bool _isTrim; /// <summary> /// トリミングサイズ /// </summary> [SerializeField] private Vector2Int _trimSize; /// <summary> /// トリミング開始地点(左下基準(0, 0)) /// </summary> [SerializeField] private Vector2 _trimStartPosition; /// <summary> /// uGUIのButtonから呼ぶ /// </summary> public void SaveCapture() { var tex = GetCapture(); // 完成画像を縮小指定なら縮小サイズにする if (_isThumbnail) { tex = CreateThumbnailTexture(tex, (int) (tex.width * _thumbnailScale), (int) (tex.height * _thumbnailScale)); } SaveImage("capture", tex); } /// <summary> /// RenderTextureからTexture2Dを生成 /// </summary> public Texture2D GetCapture() { // 後から戻せるように背景色を退避 var bgColor = _3dCamera.backgroundColor; // 背景変更する _3dCamera.backgroundColor = _backgroundColor; // いらないオブジェクトを非表示に _3dObject.SetActive(false); // 再描画する _3dCamera.Render(); // RenderTextureを取得する var targetRt = _3dCamera.targetTexture; var width = targetRt.width; var height = targetRt.height; // 現在使用している描画データを退避 var currentRt = RenderTexture.active; // 一時的に使用するRenderTextureを生成 var workRt = RenderTexture.GetTemporary(width, height, 0); // 描画対象にセット RenderTexture.active = workRt; // RenderTextureに描画する { // 背景透過用にclearで塗りつぶす(描画対象範囲が同じなら不要) GL.Clear(true, true, Color.clear); Graphics.Blit(targetRt, workRt); // ロゴが設定されていればロゴを描画する if (_logo != null) { GL.PushMatrix(); GL.LoadPixelMatrix(-width * 0.5f, width * 0.5f, height * 0.5f, -height * 0.5f); // 右下に描画 var rect = new Rect( width * 0.5f - _logo.width * _logoScale, height * 0.5f - _logo.height * _logoScale, _logo.width * _logoScale, _logo.height * _logoScale ); Graphics.DrawTexture(rect, _logo); GL.PopMatrix(); } } Vector2Int resultSize = new Vector2Int(width, height); Vector2 trimStartPosition = new Vector2(); if (_isTrim) { resultSize = _trimSize; trimStartPosition = _trimStartPosition; } // 透過書き出しするのでAlphaも含める var format = TextureFormat.ARGB32; // Texture2Dに書き出す var resultTexture = new Texture2D(resultSize.x, resultSize.y, format, false); resultTexture.hideFlags = HideFlags.DontSave; resultTexture.ReadPixels(new Rect(trimStartPosition.x, trimStartPosition.y, width, height), 0, 0, false); resultTexture.Apply(); // 退避していたRenderTextureを戻す RenderTexture.active = currentRt; // 生成したRenderTextureを破棄 RenderTexture.ReleaseTemporary(workRt); // 表示を戻す _3dCamera.backgroundColor = bgColor; _3dObject.SetActive(true); return resultTexture; } /// <summary> /// 縮小画像を作成する /// </summary> public Texture2D CreateThumbnailTexture(Texture2D origin, int thumbWidth, int thumbHeight, Material mat = null) { var currentRt = RenderTexture.active; // 縮小サイズでRenderTextureを作成 var workRt = RenderTexture.GetTemporary(thumbWidth, thumbHeight, 0); RenderTexture.active = workRt; { GL.Clear(true, true, Color.clear); Graphics.Blit(origin, workRt); } // 透過させないならRGB24で var fullTex = new Texture2D(thumbWidth, thumbHeight, TextureFormat.RGB24, false); fullTex.hideFlags = HideFlags.DontSave; fullTex.ReadPixels(new Rect(0, 0, thumbWidth, thumbHeight), 0, 0, false); fullTex.Apply(); RenderTexture.active = currentRt; RenderTexture.ReleaseTemporary(workRt); return fullTex; } /// <summary> /// Textureを画像書き出し /// </summary> private void SaveImage(string fileName, Texture2D tex) { if (!Directory.Exists(SAVE_FOLDER)) { Directory.CreateDirectory(SAVE_FOLDER); } var path = Path.Combine(SAVE_FOLDER, fileName); // 透過画像を扱うので、pngで保存 File.WriteAllBytes($"{path}.png", tex.EncodeToPNG()); } /// <summary> /// 非同期処理パターン /// </summary> private async UniTask<Texture2D> GetCaptureAsync() { var bgColor = _3dCamera.backgroundColor; _3dCamera.backgroundColor = _backgroundColor; _3dObject.SetActive(false); _3dCamera.Render(); var targetRt = _3dCamera.targetTexture; var width = targetRt.width; var height = targetRt.height; var currentRt = RenderTexture.active; var workRt = RenderTexture.GetTemporary(width, height, 0); RenderTexture.active = workRt; // RenderTextureに描画する { // 背景透過用にclearで塗りつぶす(描画対象範囲が同じなら不要) GL.Clear(true, true, Color.clear); Graphics.Blit(targetRt, workRt); // ロゴが設定されていればロゴを描画する if (_logo != null) { GL.PushMatrix(); GL.LoadPixelMatrix(-width * 0.5f, width * 0.5f, height * 0.5f, -height * 0.5f); // 右下に描画 var rect = new Rect( width * 0.5f - _logo.width * _logoScale, height * 0.5f - _logo.height * _logoScale, _logo.width * _logoScale, _logo.height * _logoScale ); Graphics.DrawTexture(rect, _logo); GL.PopMatrix(); } } // == ここまで同じ処理 == var request = AsyncGPUReadback.Request(workRt); RenderTexture.active = currentRt; RenderTexture.ReleaseTemporary(workRt); _3dCamera.backgroundColor = bgColor; _3dObject.SetActive(true); // 非同期待機 await UniTask.WaitUntil(() => request.done || request.hasError); if (request.hasError) { return null; } var format = TextureFormat.ARGB32; // Texture2Dに書き出す var resultTexture = new Texture2D(width, height, format, false); resultTexture.hideFlags = HideFlags.DontSave; var buffer = request.GetData<Color32>(); resultTexture.SetPixels32(buffer.ToArray()); resultTexture.Apply(); return resultTexture; } }まとめ
スクリプトで画像書き出しが出来るようになると、実機で3Dモデルの画像書き出しができるようになります。
もし、デザイナーさんが手動で透過処理をやっている場合は教えてみてはいかがでしょうか。
Unityに不慣れな方がいた場合はモデルキャプチャ用のツールを作ってみると大変喜ばれます。ライセンス
© Unity Technologies Japan/UCL
- 投稿日:2019-12-09T11:37:42+09:00
Realsenseの赤外線カメラを利用して簡易モーショントラッキングを行う
概要
この記事は 3D Sensor Advent Calendar 2019 の9日目の記事になります。
OptiTrack等のモーションキャプチャシステムが行っている赤外線カメラでマーカーをトラッキングして位置を追跡する手法をRealsenseの赤外線カメラと再帰性反射テープを使って作ってみたので紹介します。出来る事
再帰性反射テープを張り付けた物体の位置をトラッキング出来ます。90FPSでトラッキング出来るので素早い動きもトラッキング出来ます。これらの処理を全てUnity上で行います。
以下のような事ができます。
#RealSense
— unagi (@UnagiHuman) March 1, 2019
Realsense二台で再帰性反射材使ったモーショントラッキング。遅延も気にならないし、90FPSとれるので動きが早くてもトラッキング出来る。 pic.twitter.com/ZifZkVvRIW必要機材
合計5~6万ほどで機材一式が揃います。
- RealsenseD435
- IR投光器(850nm)
- 再帰性反射テープ
- チャンバラキングネオ(反射テープを張る対象物)
- IRハイパスフィルタ(840nm)
- Realsense用スタンド
- Realsenseをスタンドに固定する為の雲台
- USB3.0延長ケーブル
検証に利用したソフトウェア、ライブラリ
- Unity
- OpenCV for Unity
- libRealsense
トラッキング原理
RealsenseにはIRカメラが2つ付いているので、立体視による3次元座標の計算が可能です。
この計算を、赤外線投光器から照射した赤外線を再帰性反射テープ反射した反射光に対して行います。
intel公式の以下のページにステレオ画像からデプス値を計算する方法が載っていますので、これを参考にしました。
https://github.com/IntelRealSense/librealsense/blob/master/doc/depth-from-stereo.md機材構成の詳細
値段が安くて、トラッキングを実現するのに手間がかからないものを選びました。
RealsenseD435
- 自分が調べた中で赤外線カメラとしてのコスパが最強です。最大90FPSまで速度でるし、2眼なのでRealsense単体で立体視できます。
- 一応D435,D415双方とも使えますが、D435の方がカメラに余分なIRフィルターがついて無いので加工をする必要がないのと、グローバルシャッター方式なので高速に動いてるものでも問題なく撮影できるのでD435の方がお勧めです。
IR投光器(850nm)
- 赤外線ダイオードを利用して自作するのが一番安いですが、超面倒なので在りものを購入しました。それでも一台10800円なのでコスパは高いと思います。
IRハイパスフィルタ(840nm)
- ハイパスフィルタとは指定波長以上の光をカットするという意味です。これをRealsenseのIRカメラに張り付けて再帰性反射板から反射した赤外線のみを撮影します。IR投光器が850nmなのにフィルタが840nmなのは、両者の波長が一致して尚且つ値段が安いものが探しても無かったので、一番近いものを選んでいます。
再帰性反射テープ
- トラッキングするマーカーとして利用するものです。加工しやすく、どこにでも貼り付けられるテープ式のが便利です。
IR画像取得⇒3次元座標変換までの処理フロー
LibRealsenseのUnityWrapperで取得した左右のIRカメラからのIR画像からOpenCVでマーカーの座標を取得します。OpenCVはOpenCV for Unityを利用します。
1. IR画像取得
UnityWrapperのTexturesDepthAndInfraredシーンが参考になります。基本はRsDeviceのProfilesにInfraredの設定をして、RsStreamTextureRendererでIR画像のテクスチャーを取得します。ただし、必要なIR画像は左右のカメラ分ありますので、RsDeviceで左右のカメラからIR画像を取得する為の設定をします。
また、設定パラメータは取得FrameRateはMaxの90に設定してあり、後述のOpenCvForUnityのBlob検出の処理で処理落ちが発生しないように解像度は低めに設定しています。
RsDeviceからTextureを受け取るのはRsStreamTextureRendererになります。ちなみにStreamIndexが1の場合がReanselseの右のIRカメラ、2が左のIRカメラとなります。
2. OpenCVでBlob detect
OpenCVforUnityのSimpleBlobExampleシーンを参考にしました。下記に、BlobDetectでBlobのピクセル座標を取得する所までのサンプルを載せます。詳細はコメントに書きました。
BlobDetectorsample.csusing UnityEngine; using OpenCVForUnity; public class BlobDetectorSample : MonoBehaviour { private Mat _imgMat = null; private FeatureDetector _blobDetector = null; private MatOfKeyPoint _keypoints = null; private string _blobparams_yml_filepath; private Texture2D _infraredTexture; void Start() { //OpenCV for UnityのUtilsクラス。StreamingAssetsを参照する //blobparams.ymlはFeatureDetector.SIMPLEBLOBのパラメータ _blobparams_yml_filepath = Utils.getFilePath("blobparams.yml"); } /// <summary> /// RsStreamTextureRendererにバインドする /// </summary> /// <param name="InfraredTexture"></param> public void BindInfraredTexture(Texture2D InfraredTexture) { _infraredTexture = InfraredTexture; } private void Update() { if (_imgMat == null) { _keypoints = new MatOfKeyPoint(); //IRテクスチャ用のMat初期化 _imgMat = new Mat(_infraredTexture.height, _infraredTexture.width, CvType.CV_8UC1); //OpenCV For UnityのFeatureDetector _blobDetector = FeatureDetector.create(FeatureDetector.SIMPLEBLOB); //_blobparams_yml_filepathで設定したパラメータでblobdetectする為の設定 _blobDetector.read(_blobparams_yml_filepath); } Utils.fastTexture2DToMat(_infraredTexture, _imgMat); _blobDetector.detect(_imgMat, _keypoints); var keys = _keypoints.toArray(); //後は取得したkeysからblobのピクセルポイントを取得して3次元座標に変換する } }3. BlocDetectで取得したマーカーのピクセル座標を3次元座標に変換
左右のIR画像から得られたBlobからDepth計算
下記記事を参照にしてます。
https://github.com/IntelRealSense/librealsense/blob/master/doc/depth-from-stereo.md
baseLineの値やfxの値はlibrealsenceのStreamProfileから得られます。左右の対応するBlobのpixel座標から3次元座標を計算
librealsenseのコードをほぼそのままC#に移植してます。(この部分はC# wrapperには無かった。。)
https://github.com/IntelRealSense/librealsense/blob/5e73f7bb906a3cbec8ae43e888f182cc56c18692/include/librealsense2/rsutil.h#L46下記にコードを書きました。処理の流れはコメントを参照してください。
Blob3DSample.csusing UnityEngine; using Intel.RealSense; using UnityEngine.Assertions; public class Blob3DSample : MonoBehaviour { /// <summary> /// 右のIRカメラのStreamProfile /// </summary> public StreamProfile rightProfile; /// <summary> /// 左のIRカメラのStreamProfile /// </summary> public StreamProfile leftProfile; /// <summary> /// 右カメラのIR画像から算出したBlobの中心ピクセル位置。先ほどのBlobDetectSampleで計算した値をセット /// </summary> public Vector2 rightCameraBlobCenter; /// <summary> /// 左カメラのIR画像から算出したBlobの中心ピクセル位置。先ほどのBlobDetectSampleで計算した値をセット /// </summary> public Vector2 leftCameraBlobCenter; /// <summary> /// RsDevice参照 /// </summary> [SerializeField] RsFrameProvider _source = null; /// <summary> /// realsenseのカメラ内部パラメタ /// </summary> private Intrinsics _intrinsics; /// <summary> /// realsenseのカメラ外部パラメタ(baseLine参照用) /// </summary> private Extrinsics _extrinsics; // Start is called before the first frame update void Start() { _extrinsics = this.rightProfile.GetExtrinsicsTo(leftProfile); using (var profile = _source.ActiveProfile.GetStream<VideoStreamProfile>(Stream.Infrared, 1)) { _intrinsics = profile.GetIntrinsics(); } } // Update is called once per frame void Update() { var point = Vector3.positiveInfinity; CalculatePosition(ref point, _intrinsics, rightCameraBlobCenter, leftCameraBlobCenter, Mathf.Abs(_extrinsics.translation[0])); //後は得られた3次元座標をよしなに利用する } /// <summary> /// 左右のIR画像の対応点から3次元の座標を計算 /// </summary> /// <param name="point"></param> /// <param name="intrin"></param> /// <param name="pixelR"></param> /// <param name="pixelL"></param> /// <param name="baseline"></param> void CalculatePosition(ref Vector3 point, in Intrinsics intrin, in Vector2 pixelR, in Vector2 pixelL, float baseline) { var depth = CalculateDepth(intrin.fx, pixelR.x, pixelL.x, baseline); Rs2_Deproject_Pixel_to_Point(ref point, in intrin, in pixelR, depth * 0.001f); } /// <summary> /// ステレオカメラからdepthを計算 /// </summary> /// <param name="fx"></param> /// <param name="rx"></param> /// <param name="lx"></param> /// <param name="baseLine"></param> /// <param name="depthUnits"></param> /// <returns></returns> float CalculateDepth(float fx, float rx, float lx, float baseLine, float depthUnits = 0.001f) { return fx * baseLine / (depthUnits * Mathf.Abs(rx - lx)); } /// <summary> /// pixel座標とdepth値から3次元座標を計算 /// </summary> /// <param name="point"></param> /// <param name="intrin"></param> /// <param name="pixel"></param> /// <param name="depth"></param> public void Rs2_Deproject_Pixel_to_Point(ref Vector3 point, in Intrinsics intrin, in Vector2 pixel, float depth) { Assert.IsTrue(intrin.model != Distortion.ModifiedBrownConrady); Assert.IsTrue(intrin.model != Distortion.Ftheta); float x = (pixel.x - intrin.ppx) / intrin.fx; float y = (pixel.y - intrin.ppy) / intrin.fy; if (intrin.model == Distortion.ModifiedBrownConrady) { float r2 = x * x + y * y; float f = 1 + intrin.coeffs[0] * r2 + intrin.coeffs[1] * r2 * r2 + intrin.coeffs[4] * r2 * r2 * r2; float ux = x * f + 2 * intrin.coeffs[2] * x * y + intrin.coeffs[3] * (r2 + 2 * x * x); float uy = y * f + 2 * intrin.coeffs[3] * x * y + intrin.coeffs[2] * (r2 + 2 * y * y); x = ux; y = uy; } point.x = depth * x; point.y = depth * y; point.z = depth; } }トラッキング位置ブレを軽減する方法
以上の方法で取得した座標データは調整を加えないとノイズが酷く使い物になりませんが以下の方法で対策が可能です。
球形状のマーカーを利用する
- これが一番無難。球以外の形状だとカメラとの位置関係によってカメラからみるマーカーの形状が変化してしまいますが、球なら全部円形状として見え、中心位置が正確に求められるので精度が増します。ボールに赤外線反射テープを巻きつけて自作するのが一番安いです。
ローパスフィルタ
過去フレームの値を利用してローパスフィルタをかけ細かい位置ブレをフィルターします。
以下のようなフィルター関数を作って利用しました。LowPassFilter.cspublic class LowPassFilter { public Vector3 filterdValue { get; private set; } /// <summary> /// ローパスフィルター係数 /// </summary> private float lowpassK = 0.7f; private Vector3 _prePosition; public LowPassFilter(float lowpassK) { this.lowpassK = lowpassK; this._prePosition = Vector3.zero; } public void FilterData(Vector3 position) { this.filterdValue = Filter(position, this._prePosition); this._prePosition = position; } Vector3 Filter(Vector3 position, Vector3 prePosition) { return (1f - this.lowpassK) * prePosition + this.lowpassK * position; } }Realsneseの精度の限界について
- 精度
- 2眼カメラの3次元位置の計算精度はカメラ同士の距離が離れているほど高くなりますが、Realsensは5cmほどしか離れておらず、Realsenseとマーカーの距離がだいたい2mを超えるとブレ始めます。
- なので、もっと精度を求めるのであれば単純に2眼の距離を物理的に離すのが一番簡単です。
- その場合は2眼の距離が離れている別製品のIRステレオカメラを購入するのがよいかも。
まとめ
Realsenseでモーショントラッキングする仕組みをDIYしました。
明日12/10(火)はxbarusuiさんで「初心者が Unity で始める Azure Kinect」です。お楽しみに!
- 投稿日:2019-12-09T10:19:07+09:00
音声解析リップシンクミドルウェア「CRI ADX LipSync」を使ってみた
本記事は、サムザップ Advent Calendar 2019 #2 の12/7の記事です。
CRI ADX LipSyncとは
2019年11月末に深層学習を用いた音声解析によるリップシンクをするためのミドルウェア『CRI ADX LipSync』がCRI・ミドルウェア社からリリースされました。2019年12月頭現在、対応しているゲームエンジンはUnityのみですが、いずれ他のゲームエンジンにも対応していく予定とのことです。
今回リリースされた音声解析機能には、再生した音声を入力としてリアルタイムに口の形状情報を出力するものと、予め音声から口の形状情報をテキストデータとして出力するものとがあります。
前者は、CRIWARE SDKに含まれており同社が出しているサウンド演出を手掛ける『CRI ADX2』というミドルウェアと組み合わせて手軽に実装することができます。こちらは、CRIWAREユーザーであればタイトルリリース後も無料で使用することができるようです。
後者は、専用のコマンドツール(Windows版のみ)を使うことになり、出力されたデータはADX2のツール上で編集することも可能です。こちらは、CRIWAREユーザーであってもタイトルリリース後は料金が発生するようです。本記事では、Unityで『CRI ADX2』と『CRI ADX LipSync』を組み合わせて音声データから口の形状情報をリアルタイムに得る方法を紹介したいと思います。
フォルマントベースの音声解析
音声からリアルタイムに口形状情報を得るために用いられる特徴量は、主に次の2つがあります。
・音声の振幅値(音量)
・音声のフォルマント(周波数のピーク)振幅値ベースのリップシンクは、実装が割と楽な反面、得られるデータが1次元であるため単純な口の開閉運動しか表現できず不自然さを感じやすくなります。
一方で、フォルマントベースのものでは、2次元データ(F1, F2周波数)から母音(a, i, u, e, o)を推定することができるため、口の上下の開閉だけでなく左右の伸縮にもマッピングすることで、より自然な表現が可能となります。『CRI ADX LipSync』を使うと、フォルマントベースのリップシンクを手軽に実現することができます。
唇音の判定もできる
自然なリップシンクを実現する上では、母音が推定できただけでは実はまだ足りないようです。口を閉じるところでちゃんと閉じさせることができないケースがあります。
「ま」や「ば」などの唇音と呼ばれる音です。唇音は、口を一度閉じてから発音します。
例えば、母音の推定だけで「ママ」という単語の発音をリップシンクさせると、「aa」と口が開きっぱなしになってしまうのです。この唇音の判定まで自前で実装するとなると、結構大変そうです。
『CRI ADX LipSync』では、なんと唇音であるかどうかも判定してくれるようです。実際に使ってみる
リアルタイムに再生される音声を解析する方式と、予め解析して口パクデータを作っておくオフライン方式とが用意されているが、
今回は、リアルタイムでの解析を試してみました。使用したツールのバージョンは、以下の通りです。
Unity: 2019.2.5f1
CRIWARE SDK for Unity: 3.00.00リアルタイム解析による口形状情報取得までの流れ
1.アナライザーの初期化
ADX2のAtomライブラリを用いて再生した音声から口形状情報を取得するための解析器であるCriLipsAtomAnalyzerを初期化します。// インスタンス生成 var atomAnalyzer = new CriLipsAtomAnalyzer(); // 無音と判定する最大音量(0以下のdB値)の設定 atomAnalyzer.SetSilenceThreshold(-40); // 解析対象の音声データのサンプリング周波数を設定(16000Hz以上) atomAnalyzer.SetSamplingRate(48000);2.アナライザーのプレイヤーへのアタッチ
AtomライブラリのプレイヤーであるCriAtomExPlayerにCriLipsAtomAnalyzerのインスタンスをアタッチします。var player = new CriAtomExPlayer(); atomAnalyzer.AttachToAtomExPlayer(player);3.アナライザーから口形状情報の取得
CriLipsAtomAnalyzerからCriLipMouthクラスの口形状情報を格納する構造体CriLipsMouth.InfoおよびCriLipsMouth.MorphTargetBlendAmountAsJapaneseを取得します。// 縦横形状の組み合わせ情報の取得 var info = new CriLipsMouth.Info(); atomAnalyzer.GetInfo(out info); // 日本語5母音の組み合わせ情報の取得 var blendAmount = new CriLipsMouth.MorphTargetBlendAmountAsJapanese(); atomAnalyzer.GetMorphTargetBlendAmountAsJapanese(out blendAmount);取得できる口形状情報の種類
CriLipsAtomAnalyzerからは、次の2種類の口形状情報を取得することができます。
・縦横形状の組み合わせ情報
構造体: CriLipsMouth.Info
構造体のフィールド:
フィールド名 取得できる値 lipWidth 口の幅 (0.0f~1.0f) lipHeight 口の高さ (0.0f~1.0f) tonguePosition 舌の位置 (0.0f~1.0f) isLipWidthReleased 口の幅が閉じ状態に遷移中かどうか isLipHeightReleased 口の高さが閉じ状態に遷移中かどうか isLipToungueReleased 舌の位置が閉じ状態に遷移中かどうか ・日本語5母音の組み合わせ情報
構造体: CriLipsMouth.MorphTargetBlendAmountAsJapanese
構造体のフィールド:
フィールド名 取得できる値 a 「あ」のブレンド量 (0.0f~1.0f) i 「い」のブレンド量 (0.0f~1.0f) u 「う」のブレンド量 (0.0f~1.0f) e 「え」のブレンド量 (0.0f~1.0f) o 「お」のブレンド量 (0.0f~1.0f) ※ 同時刻においては、5母音の内2母音のブレンド量が0より大きい値として取得できます。
結果
『CRI ADX LipSync』のサンプルプロジェクトについてくる音声を解析してどのようなデータが得られるか調べてみました。サンプル音声は、女性の声で「私が死んだら、代わりはいるのでしょうか。」と話しているものを使いました。
まずは、分かりやすいので日本語5母音の組み合わせ情報の時間推移を示します。
図を見てみると、「あ」「い」「う」については比較的精度高く推定できていそうですが、この音声では「え」と「お」はほとんど検出されなかったようで、子音や前後の音との組み合わせによっては推定が難しいケースもあるようです。そもそも、フォルマント的にも各母音のフォルマントの分布は完全に分離されているという訳ではなく、一部が重複した状態で分布していることも、推定を誤ってしまう原因として考えられます。次に、縦横形状の組み合わせ情報の時間推移を見てみます。
口の高さの推移の図を見てみると、口を縦に大きく開く「a」や「o」の音で極大となっており、全体的に良く追従できているのが分かります。さらに、図の赤丸で示した「だ」と「ら」の間では、一瞬口を閉じている状態のデータを返しており、唇音ではないが間で口を閉じる発音にも対応できていることが伺えます。最後に、CriLipsAtomAnalyzerクラスのGetRmsメソッドから取得した音圧の強さを表すRMS(Root Means Square)値の時間推移です。
こちらのデータだけを使ってリップシンクをさせるのは難しそうですが、口形状情報と組み合わせて使うと音量による口の開き具合の影響度を大きくしたい場合などに役立つかもしれないです。CriLipsAtomAnalyzerクラスのGetVolumeメソッドでも音量(dB値)が取得できるようなので、今後そちらも検証してみたいと思います。まとめ
自然なリップシンクを自前で実装しようとすると、実装も難しく計算負荷も高くなってしまいがちですが、『CRI ADX2』による再生機構と『CRI ADX LipSync』が提供する解析機構を組み合わせて使うことで手軽に品質の高いリップシンクを実現することができそうだということが分かりました。
実機でのプロファイリングで問題がなければ、プロジェクトでの導入も検討していきたいです。参考
・キャラクターをより魅力的に!ゲーム向けリップシンクミドルウェア CRIWARE公式ページより
・音声解析リップシンクミドルウェアCRI ADX LipSync明日は @kojima_akira さんの記事です。
- 投稿日:2019-12-09T08:17:29+09:00
8頂点でサイリウムを綺麗に表現する
この記事では以前にちょっとした実験で実装してみた、8頂点でサイリウムを表現する方法をご紹介します。
Unityプロジェクトはこちらから参照できます!
https://github.com/kaneta1992/EightVerticesPsyllium
以下のgif動画はこの記事で紹介する8頂点を利用した方法(ON)と、オーソドックスな方法(OFF)を比較したものです。結構よい感じになっているのではないでしょうか!(サンプルのため動きのクォリティ等はご了承ください
)
まずはよくある4頂点で実装する場合
実装はとても簡単で、頂点シェーダーを利用してビルボードのようなものを実装します。ただし通常のビルボードと異なり、Y軸の方向をロックした上で面をカメラ方向に向ける必要があります。
それを頂点シェーダーで実装したのが以下のコードです。
v2f vert (appdata v) { v2f o; float4x4 mat = unity_ObjectToWorld; float3 barUp = mat._m01_m11_m21; float3 barPos = mat._m03_m13_m23; // Y軸をロックして面をカメラに向ける姿勢行列を作る float3 cameraToBar = barPos - _WorldSpaceCameraPos; float3 barSide = normalize(cross(barUp, cameraToBar)); float3 barForward = normalize(cross(barSide, barUp)); mat._m00_m10_m20 = barSide; mat._m01_m11_m21 = barUp; mat._m02_m12_m22 = barForward; float4 vertex = float4(v.vertex.xyz, 1.0); vertex = mul(mat, vertex); o.vertex = mul(UNITY_MATRIX_VP, vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }unity_ObjectToWorldから上方向のベクトルとサイリウムの位置を取得して、それを元に新しい姿勢の計算を行い、頂点のワールド変換を行います。
これだけでもそれなりの品質になりますが、視線とサイリウムが平行になるにつれて、ペラペラな板ポリであることがバレてしまいますね。
次の方法で、これをできるだけ目立たないように改善してみます。
8頂点にしてペラ感を改善する
ここからは最初に書いた通り、4頂点と少しの工夫を追加して品質を向上します。
頂点を追加する
4頂点では表現力に限界があるので、サイリウムの上下の半円の場所に頂点を追加して、8頂点にします。
頂点にデータを埋め込む
変形で使用するため、先ほど追加した頂点に半円部分の半径をuv2の中に事前に埋め込んでおきます。
頂点シェーダーで変形をする都合で上部は正、下部は負の半径にします。
今回は簡単な形状なのでスクリプトでメッシュを生成しました。
mesh.uv2 = new Vector2[] { new Vector2 (-radius, 0), new Vector2 (0, 0), new Vector2 (-radius, 0), new Vector2 (0, 0), new Vector2 (0, 0), new Vector2 (0, 0), new Vector2 (radius, 0), new Vector2 (radius, 0), };メッシュを生成しているスクリプトの全文はこちらです。
カメラ位置を利用してメッシュを変形する
この段階ではまだペラペラなので、追加した頂点をビルボードのように、カメラの位置を見て、常に真正面に見えるように変形します。
さらに視点を固定したまま横から見ると以下のようになってます。
これを実現するために、上で作った4頂点バージョンのサイリウムの頂点シェーダーに二行追加します。
v2f vert (appdata v) { v2f o; float4x4 mat = unity_ObjectToWorld; float3 barUp = mat._m01_m11_m21; float3 barPos = mat._m03_m13_m23; // Y軸をロックして面をカメラに向ける姿勢行列を作る float3 cameraToBar = barPos-_WorldSpaceCameraPos; float3 barSide = normalize(cross(barUp, cameraToBar)); float3 barForward = normalize(cross(barSide, barUp)); mat._m00_m10_m20 = barSide; mat._m01_m11_m21 = barUp; mat._m02_m12_m22 = barForward; float4 vertex = float4(v.vertex.xyz, 1.0); vertex = mul(mat, vertex); // 追加ここから // 事前に頂点に入れておいた半径を使って、半円部分がカメラの真正面になるように変形する float3 offsetVec = normalize(cross(cameraToBar, barSide)); vertex.xyz += offsetVec * v.uv2.x; // ここまで o.vertex = mul(UNITY_MATRIX_VP, vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; }8頂点にしてシェーダーを二行加えただけで品質がかなり向上しました!
シェーダーの全文はこちらです。
さいごに
今回実験で実装してみましたが、思いのほか綺麗に見えたので記事にしてみました。
ちょっとした工夫で綺麗に見せることができるものがまだまだあると思うので今後も目を凝らして観察していきたいともいます!
- 投稿日:2019-12-09T03:21:28+09:00
Unity セーブ・ロードの仕組み
ゲームをする側でいる時、前回プレイした時に獲得したアイテムがまだ手元に残っているのを当たり前のように思っていましたが、いざ自分自身がゲームを作る側になると、どういう仕組みでそれが成立しているのか不思議なります。今回はアイテムなどのオブジェクトのセーブ/ロードに関しての話です。(今回話すのはローカルセーブについてです。)
実現したいこと
- セーブ / ロード機能の実装
現状
- セーブ/ロードがどういう仕組みか分からない
- セーブ/ロード機能の実装方法が分からない
仕組みの大まかな説明
セーブするとはどのような仕組みによって成立しているのでしょうか?
ざっくり言うとセーブ対象となるオブジェクトの状態(クラスのフィールドと思って大丈夫かと)を保存することです。オブジェクトの状態を保存をする際にシリアル化と言う重要なプロセスがあります。
シリアル化についての説明は、Microsoft Documentによると
シリアル化は、オブジェクトを格納するか、メモリ、データベース、またはファイルに転送するためにバイト ストリームに変換するプロセスです。 その主な目的は、必要なときに再作成できるように、オブジェクトの状態を保存しておくことです。 逆のプロセスは、逆シリアル化と呼ばれます。
引用したドキュメントにはわかりやすい図もあったので載せておきます
Unityではセーブ機能を実装するための様々な方法があります。
- PlayerPrefs
- ScriptableObjects
- json / xml
- custom binary file
それぞれ長所、短所があり一概にどれが一番良いと言うことはできませんし、自分にあった方法を見つけていただければと思います。
以下の表にざっくりとまとめました。
実装方法 概要 長所 短所 備考 PlayerPrefs Player Preferences(略してPlayerPrefsかな)を保存、アクセスできる UnityEngineが提供しているため手軽に扱える。 使えるデータ型の制限(Int, Float, Stringのみ)がある。
単純な記録(スイッチのオンオフを覚えておくとか)には向いているが、複雑な処理には向いていないUnity Document ScriptableObjects クラスのインスタンスとは独立し、データを大量に格納できるコンテナ アセットとして扱える。
メモリ消費量が小さい。主に開発側のためのものであり、プレイヤーのプレイ中におけるセーブのための機能ではない(つまりプレイヤーの進捗とか保存できない)。 Unity Documentation external files(json/xml) よく使われるマークアップ言語 簡単に修正できる 簡単に修正できる custom binary file バイナリ形式でシリアライズ、デシリアライズを行う 自由度が高い。
バイナリ形式であるため安全性が高い。コードを書く量が多い。 サンプル
githubにサンプルを上げていますので、興味があればご自由に見てください。
本サンプルでは、Custom Binary Fileでセーブ機能を実装しています。実行環境
- macOS Catalina ver10.15.1
- Unity 2019.2.10f1
概要
シーン1: 新しくゲームを始めるか、登録済みのプレイヤーをロードするかの選択。(ロードを選択するとシーン3へ行きます)
シーン3: ゲームシーン。セーブデータが正しく保存されているか確かめるためにセーブデータをロードしてプレイヤーに反映します。
全部を紹介すると長くなるのでセーブ、ロードに関するところをピックアップして以下に紹介します。
シーン2
シーン1はシーン遷移のみなので飛ばします。
まずセーブするプレイヤーのデータ
SavePlayerData.cs[System.Serializable] public class SavePlayerData { public string name; public int age; public string color; }セーブするクラスにはSystem.Serializableと言う属性をつけることでシリアル化を可能にします。
Monobehaviourを継承しているとシリアル化できないの注意しましょう。次にユーザーの入力をもとにセーブデータを作りシリアル化する箇所です。
SaveManager.csusing System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using UnityEngine; using UnityEngine.UI; public class SaveManager : MonoBehaviour { // ----- 一部抜粋 ----- // public void OnSaveNewPlayer() { // セーブデータ作成 SavePlayerData player = CreateSavePlayerData(); // バイナリ形式でシリアル化 BinaryFormatter bf = new BinaryFormatter(); // 指定したパスにファイルを作成 FileStream file = File.Create(SaveFilePath); // Closeが確実に呼ばれるように例外処理を用いる try { // 指定したオブジェクトを上で作成したストリームにシリアル化する bf.Serialize(file, player); } finally { // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。 if (file != null) file.Close(); } } // 入力された情報をもとにセーブデータを作成 private SavePlayerData CreateSavePlayerData() { SavePlayerData player = new SavePlayerData(); player.name = nameInput.text; player.age = int.Parse(ageDropdown.options[ageDropdown.value].text); player.color = colorDropdown.options[colorDropdown.value].text; return player; } // ----- 一部抜粋 ----- // }シーン3
セーブしたファイルをロードする部分はこちらです。
LoacManager.cspublic class LoadManager : MonoBehaviour { // ----- 一部抜粋 ----- // private void LoadPlayer() { if (File.Exists(SaveFilePath)) { // バイナリ形式でデシリアライズ BinaryFormatter bf = new BinaryFormatter(); // 指定したパスのファイルストリームを開く FileStream file = File.Open(SaveFilePath, FileMode.Open); try { // 指定したファイルストリームをオブジェクトにデシリアライズ。 SavePlayerData player = (SavePlayerData)bf.Deserialize(file); // 読み込んだデータを反映。 var playerObject = Instantiate(playerPrefab) as GameObject; playerObject.GetComponent<PlayerController>().Init(player.name, player.age, player.color); } finally { // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。 if (file != null) file.Close(); } } else { Debug.Log("no load file"); } } // ----- 一部抜粋 ----- // }それほど難しくないですね!
シリアライズなど使えるようになると、ランキングやら簡単なログイン機能の実装やら色々できることの幅が広がりそうですね!
Next Step
- 他のPlayerPrefs/ScriptableObjects/JSON/XMLとの違いを試してみる
- クラウドセーブ
- セーブファイルと暗号化について
- オートセーブ機能について
参考文献
Unite 2016 - Best Practices in Persisting Player Data on mobile
Unite Europe 2017 - How Unity's Serialization system works
Saving Game Data in Unity
How to Save and Load a Game in Unity
シリアル化
Script Serialization
オブジェクトのシリアル化コメント
以下の観点でコメントいただけると嬉しく思います!
- 間違い、不備
- 「こういう状況では、このセーブ/ロード方法が良いよ!」
- 投稿日:2019-12-09T01:23:43+09:00
【Unity】「機動戦士ガンダムVS.」シリーズっぽいカメラの動きを作ってみる
はじめに
本記事ではUnityで「機動戦士ガンダムVS.」シリーズにあるような、操作キャラクターを視界に入れつつ、ターゲットに向き続けるTPSカメラを作ってみます。
近い動きで「ディシディア ファイナルファンタジー」や「Fate/Grand Order Arcade」のようなカメラにも応用できるんじゃないかと思います。完成イメージ
デモプロジェクト
GitHubにアップロードしています。
unity-tps-lock-on-camera位置と向きのロジック
カメラの動きを作るときは先に位置を考え、その後に向きを考えるのが進めやすいと思います。
位置
まずカメラの位置ですが、下図のようにターゲットと操作キャラを結んだ線上、操作キャラ後方の少し上辺りにいる感じがしますね。
なので、操作キャラ→ターゲットのベクトル$\vec{V_{tgt}}$を基準に、後ろ距離と高さを示すベクトル$\vec{V_{ofsLocal}}$分ずらせば欲しい位置が得られそうです。ただそれは$\vec{V_{tgt}}$を前方とするローカル空間での話なので、ワールド座標に直す必要があります。
あるベクトルを座標系(の一部)とみなしてそれを基準に位置を得る方法ですが、クォータニオンを掛けることでベクトルを回転することができるという性質を利用します。
$\vec{V_{tgt}}$を前方とするクォータニオン$q_{tgt}$を得ることができれば、ワールド座標でずらすべき位置ベクトル$\vec{V_{ofs}}$は、\vec{V_{ofs}} = q_{tgt} \times \vec{V_{ofsLocal}}となります。
幸いUnityにはQuaternion.LookRotationという便利なAPIが用意されていますので、$q_{tgt}$を得るのは難しくありません。
カメラの位置を算出するまでのコードイメージはこんな感じになります。// ターゲットへのベクトル Vector3 vTgt = target.position - player.position; // ターゲットへのベクトルを前方とするクォータニオン // 第二引数はワールド空間的な上(Vector3.up)でいいので省略 Quaternion qTgt = Quaternion.LookRotation(vTgt); // ずらすべき位置ベクトル // ずらしたい量をここでは後方5、高さ2とした場合 Vector3 vOfs = qTgt * new Vector3(0f, 2f, -5f); // 最終的なカメラ位置(ワールド座標) Vector3 cameraPosition = player.position + vOfs;向き
次に向きですが、こちらはシンプルです。
常にターゲットに向くだけなので、カメラの位置が決まればQuaternion.LookRotationにカメラ→ターゲットのベクトルを前方ベクトルとして渡してあげれば得られます。// ターゲットへの向き Quaternion cameraRotation = Quaternion.LookRotation(target.position - cameraPosition);プロトタイプ完成
変数名を分かりやすくして、MonoBehaviourにのっけたコードに直すとこんな感じです。
ここまでで操作キャラの後方を維持しつつ、ターゲットを見続けるカメラができました。TpsLockOnCameraPrototype.csusing UnityEngine; public class TpsLockOnCameraPrototype : MonoBehaviour { /// <summary> /// 取りつくキャラクター /// </summary> [SerializeField] private Transform _attachTarget = null; /// <summary> /// 取りつくキャラクターからのカメラオフセット位置 /// </summary> [SerializeField] private Vector3 _attachOffset = new Vector3(0f, 2f, -5f); /// <summary> /// 注視ターゲット /// </summary> [SerializeField] private Transform _lookTarget = null; /// <summary> /// 現在の注視点 /// </summary> private Vector3 _lookTargetPosition = Vector3.zero; private void LateUpdate() { _lookTargetPosition = _lookTarget.position; // ターゲットへのベクトル Vector3 targetVector = _lookTargetPosition - _attachTarget.position; // ターゲットへのベクトルを前方とするクォータニオン Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation; // 位置と向き Vector3 position = _attachTarget.position + targetRotation * _attachOffset; Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position); transform.SetPositionAndRotation(position, rotation); } }ターゲット切り替えのロジック
続いてターゲット切り替えの動きを考えます。
最終的にはターゲットとして指定しているtransformを変えればいいわけですが、単純に変えるとカメラが位置と向きをがっつりワープすることになるので、プレイヤーは切り替わり前後で混乱してしまいますよね。滑らかに繋ぎたいところです。ターゲット切り替え
処理の流れを考えます。
まず、元々見ていたターゲットは、切り替わり動作中も含めて無視としたいので、切り替え開始の瞬間の位置$P_{old}$だけ覚えておけばよさそうです。新しく見るターゲットは切り替わり動作中も含めてその位置$P_{new}$を追従し続けたいです。
なので$P_{old}$は固定位置、$P_{new}$はTransformのpositionとして、$P_{old}$から$P_{new}$へ補間しつつ一定時間かけて移動すればよさそうです。
(見る位置を滑らかに変えることで、カメラの位置と向きもスムーズに変化するよねという考え方です。)位置の補間にはVector3.Lerpのメソッドが使えます。(好みでイージングしてもいいかもしれません。)
よって、先程のコードにターゲット変更の処理を組み込んだ最終版はこのようになります。TpsLockOnCamera.csusing UnityEngine; public class TpsLockOnCamera : MonoBehaviour { /// <summary> /// 取りつくキャラクター /// </summary> [SerializeField] private Transform _attachTarget = null; /// <summary> /// 取りつくキャラクターからのカメラオフセット位置 /// </summary> [SerializeField] private Vector3 _attachOffset = new Vector3(0f, 2f, -5f); /// <summary> /// 注視ターゲット /// </summary> [SerializeField] private Transform _lookTarget = null; /// <summary> /// ターゲットがいないときの注視点 /// </summary> [SerializeField] private Vector3 _defaultLookPosition = Vector3.zero; /// <summary> /// ロック切り替え時間 /// </summary> [SerializeField] private float _changeDuration = 0.1f; /// <summary> /// ロック切り替えタイマー /// </summary> private float _timer = 0f; /// <summary> /// 現在の注視点 /// </summary> private Vector3 _lookTargetPosition = Vector3.zero; /// <summary> /// ロックを移すときの最後の注視点 /// </summary> private Vector3 _latestTargetPosition = Vector3.zero; /// <summary> /// ターゲット切り替え /// </summary> /// <param name="target"></param> public void ChangeTarget(Transform target) { _latestTargetPosition = _lookTargetPosition; _lookTarget = target; _timer = 0f; } private void LateUpdate() { var targetPosition = _lookTarget != null ? _lookTarget.position : _defaultLookPosition; // 現在の注視点を更新 if (_timer < _changeDuration) { _timer += Time.deltaTime; _lookTargetPosition = Vector3.Lerp(_latestTargetPosition, targetPosition, _timer / _changeDuration); } else { _lookTargetPosition = targetPosition; } // ターゲットへのベクトル Vector3 targetVector = _lookTargetPosition - _attachTarget.position; // ターゲットへのベクトルを前方とするクォータニオン Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation; // 位置と向き Vector3 position = _attachTarget.position + targetRotation * _attachOffset; Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position); transform.SetPositionAndRotation(position, rotation); } }おわりに
このタイプのカメラ挙動はカメラ操作がシンプルになるというメリットがありますね。
拙い説明であることに加え、細かくは再現しきれておらず申し訳ないですが、少しでも何かの参考になれば幸いです。
また、説明の間違いやソースコードの不具合などあるかもしれません。その場合は是非ご指摘いただけますと幸いです。
- 投稿日:2019-12-09T01:15:00+09:00
Bitrise CLIでiOSターゲットのUnityプロジェクトをアーカイブ
はじめに
こんにちわ、 @kiy0p0n です。
Diverse Advent Calendar 2019の9日目の記事です。前書き
今回はUnityでモバイルゲーム開発の際のCIについての話題です。
JenkinsでiOS/Androidのデプロイを自動化する記事はたくさんあるのですが、サーバーレスでスクリプトを書かずとも手軽にCI始めたいなと思った際に、bitriseでiOS/Androidのデプロイを自動化できるのでは?
ということをちょっと調べて実際にやってみた内容を今回は記事としてまとめました。やったこと
bitrise CLIを用いてiOSターゲットのUnityプロジェクトのビルド ~ ipa生成
環境
env version OS macOS Catalina(10.15.1) bitrise CLI 1.36.0 unity 2019.1 xcode 11.2 フロー
- bitrise CLIのインストール
- Unityプロジェクト
- ビルドスクリプトの追加
- プロジェクト設定
- ビルドステップの追加
- ipaアーカイブステップの追加
内容
1. bitrise CLIのインストール
homebrewでインストールします。
その後、セットアップコマンドでツール群をインストールします。$ brew update && brew install bitrise # install log $ bitrise -v 1.36.0 $ bitrise setup # setup logbitriseの処理は
bitrise.yml
に記述していきます。
次の内容のbitrise.yml
を作成し、実行してみます。
workflows
でワークフローを定義します。ワークフローはいくつかの処理を行うステップをまとめたものです。
以下でのtest
はワークフロー名になります。実行時にワークフローを指定します。format_version: 1.3.1 default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git workflows: test: steps: inputs: - content: "echo 'Hello World!'"
testワークフローの実行結果
$ tree . . └── bitrise.yml $ cat bitrise.yml format_version: 1.3.1 default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git workflows: test: steps: - script@1.1.5: inputs: - content: "echo 'Hello World!'" $ bitrise run test ██████╗ ██╗████████╗██████╗ ██╗███████╗███████╗ ██╔══██╗██║╚══██╔══╝██╔══██╗██║██╔════╝██╔════╝ ██████╔╝██║ ██║ ██████╔╝██║███████╗█████╗ ██╔══██╗██║ ██║ ██╔══██╗██║╚════██║██╔══╝ ██████╔╝██║ ██║ ██║ ██║██║███████║███████╗ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚══════╝╚══════╝ version: 1.36.0 INFO[22:43:09] bitrise runs in Secret Filtering mode INFO[22:43:09] Running workflow: test Switching to workflow: test +------------------------------------------------------------------------------+ | (0) script@1.1.5 | +------------------------------------------------------------------------------+ | id: script | | version: 1.1.5 | | collection: https://github.com/bitrise-io/bitrise-steplib.git | | toolkit: bash | | time: 2019-12-08T22:43:11+09:00 | +------------------------------------------------------------------------------+ | | Hello World! | | +---+---------------------------------------------------------------+----------+ | ✓ | script@1.1.5 | 2.56 sec | +---+---------------------------------------------------------------+----------+ +------------------------------------------------------------------------------+ | bitrise summary | +---+---------------------------------------------------------------+----------+ | | title | time (s) | +---+---------------------------------------------------------------+----------+ | ✓ | script@1.1.5 | 2.56 sec | +---+---------------------------------------------------------------+----------+ | Total runtime: 2.56 sec | +------------------------------------------------------------------------------+ Submitting anonymized usage informations... For more information visit: https://github.com/bitrise-io/bitrise-plugins-analytics/blob/master/README.md2. Unityプロジェクト
ビルドスクリプトの追加
Unityはコマンドラインから実行できようになっています。
Unity マニュアル - コマンドライン引数
マニュアルにライセンスのやりとり方法が書いてありますので、Proを使う場合は参考にしてください。以下のコマンドで、プロジェクト内のスクリプトを実行できます。
スクリプトにビルド処理を記述することで、Unity Editorを起動せずにビルドを行うことができます。$ /Applications/Unity/Unity.app/Contents/MacOS/Unity \ -nographics \ -quit \ -batchmode \ -logFile \ -projectPath #{プロジェクトパス} \ -executeMethod #{実行するスクリプトのクラス}.#{実行する関数} # 追加のオプションがある場合は続けて記述ビルドを行うクラスを実装します。
ビルド先のオプションを-output
で受け取れるようにしてます。
Assets/Bitrise.cs
#if UNITY_EDITOR using UnityEditor; using System.Linq; using System; class Bitrise { public static void Build() { BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions(); string[] args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { if (args[i].Equals("-output")) buildPlayerOptions.locationPathName = args[i + 1]; } var activeScenes = EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray(); buildPlayerOptions.scenes = activeScenes; buildPlayerOptions.target = BuildTarget.iOS; buildPlayerOptions.options = BuildOptions.None; BuildPipeline.BuildPlayer(buildPlayerOptions); } } #endif
バッチモードでのビルド実行
$ pwd /Users/kiy0p0n/deploy-test $ tree "Assets" Assets ├── Bitrise.cs ├── Bitrise.cs.meta ├── Scenes │ ├── Test.unity │ └── Test.unity.meta └── Scenes.meta 1 directory, 5 files $ /Applications/Unity/Unity.app/Contents/MacOS/Unity \ -nographics \ -quit \ -batchmode \ -logFile \ -projectPath /Users/kiy0p0n/deploy-test \ -executeMethod Bitrise.Build -output /Users/kiy0p0n/deploy-test/Build # build logプロジェクト設定
Unity上でiOSの署名周りの設定をする場合は参考にしてください。
Unity上でなくとも、bitrise CLIのapiアーカイブのステップでも設定可能です。
わかりづらい項目だけピックアップして記載してます。
Project Settings > Player > iOS Traget(タブ選択) > Other Settings
から署名に必要なパラメータを設定します。Singing TeamID
Singing TeamID
を設定します。TeamIDはApple Developer - Accountページから取得できます。
フォーマット例:AS345R67R1
iOS Provisioning Profile
任意のProvisioning Profileを設定する場合は、以下のどちらかでUUIDを設定できます。
- Provisioning Profileをダウンロードし、
Browse
ボタンでダウンロードした.mobileprovision
ファイルを選択- Provisioning Profileをダウンロードし、テキストエディタで開き、
UUID
の値を取得するフォーマット例:
c5be4123-1234-4f9d-9843-0d9be985a068
ビルドステップの追加
bitrise.yml
にUnityプロジェクトのビルドのステップを追加します。format_version: 1.3.1 default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git workflows: test: steps: - script@1.1.5: inputs: - content: | #!/bin/bash /Applications/Unity/Unity.app/Contents/MacOS/Unity -nographics -quit -batchmode -logFile -projectPath /Users/kiy0p0n/deploy-test -executeMethod Bitrise.Build -output /Users/kiy0p0n/deploy-test/Build3. ipaアーカイブステップの追加
bitriseのツールに
xcode-achive
というアーカイブの記述を簡易にできるモノがあるので利用します。
基本的な記述は以下のようになります。- xcode-archive@2.7.0: inputs: - output_tool: xcodebuild - export_method: development - output_dir: #{ipaの出力先} - project_path: #{xcodeproj or xcworkspaceのパス} - scheme: #{アーカイブするスキーマ} - team_id: #{署名時のチームID} - force_provisioning_profile: #{任意のProvisioning ProfileのUUID} # その他のオプション
team_id
やforce_provisioning_profile
オプションは、Unity上で署名関連の設定が済んでいる場合は不要です。Team IDやProvisioning ProfileのUUIDは、プロジェクト設定と同じ形式のモノになります。
細かいその他オプションはソース元のステップを参照してください。まとめ
最終的な
bitrise.yml
とワークフローの実行$ cat bitrise.yml format_version: 1.3.1 default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git workflows: test: steps: - script@1.1.5: inputs: - content: | #!/bin/bash /Applications/Unity/Unity.app/Contents/MacOS/Unity -nographics -quit -batchmode -logFile -projectPath /Users/kiy0p0n/deploy-test -executeMethod Bitrise.Build -output /Users/kiy0p0n/deploy-test/Build - xcode-archive@2.7.0: inputs: - output_tool: xcodebuild - export_method: development - output_dir: /Users/kiy0p0n/deploy-test - project_path: /Users/kiy0p0n/deploy-test/Build/Unity-iPhone.xcodeproj - scheme: Unity-iPhone - compile_bitcode: "no" $ bitrise run test # 以下ビルドログ最後に
手元でコマンド一つでipaまでビルドできました!
BitriseにはdeploygateなどのデプロイツールへのアップロードやSlackといったチャットツールへの投稿を簡易に設定できるツールも用意されているので、色々利用してデプロイ周りを便利にしていきたいです。
また、Bitriseの公式Blogにて、BitriseでUnityビルドができる方法が紹介されてるので、今回の続きとして、ローカル環境ではなくクラウドで実行できるようにしたいです。
- 投稿日:2019-12-09T00:59:19+09:00
Unity DOTSでブロック崩しを作る
はじめに
この記事はブロック崩しをUnity DOTSで作る方法を解説したものです。
また、この記事よりも初歩的な内容に関しては
Unity DOTSとUnity Physicsでただ単純に球を動かしてみる
に書かせて頂きました。
少しでも参考になれば幸いです。注意点
- 本記事ではpreview packageを多く使用しています。今の実装と本記事の実装が大幅に異なる可能性があるのでご注意下さい。
- 本記事ではGameObject/Component を使用したHybrid ECSを前提としています。
- Unity DOTS(Unity ECS, C# Job System, Burst Compiler) やUnity Physics に関する詳しい説明は行わないのでご注意下さい。
環境
- macOS Catalina 10.15.1
- Unity 2019.3.0f1
- Entities preview 0.3.0
- Burst preview 1.2.0
- Jobs preview 0.2.1
- Mathematics 1.1.0
- Hybrid Renderer preview 0.3.0
- Unity Physics preview 0.2.5
実装
Wallを作る
Hierarchy > Create Empty で空のオブジェクトを作り、Wallと名前を付けます。
Positionは (0, 0, 0)としておきます。Wallの子オブジェクトとしてCubeの3D Objectを4つ作成し、
Positionをそれぞれを(-3, 0, 0), (3, 0, 0), (0, 0, 4.75), (0, 0, -4.75)に、
Scaleをそれぞれ(0.5, 0.5 , 10), (0.5, 0.5, 10), (5.5, 0.5, 0.5), (5.5, 0.5, 0.5)に設定します。ついでにMainCameraも Positionを(0, 10, 0), Rotationを(90, 0, 0)に設定しておきます。
全てのCubeに付いているBox Colliderをそれぞれ削除し、Physics ShapeをそれぞれのCubeオブジェクトにAdd Componentします。
そして最後にWallオブジェクトにConvertToEntityをAdd Componentします。
Paddleを作る
Cubeの3D Objectを作成し、Positionを(0, 0, -4)、Scaleを(1.5, 0.5, 0.5)に設定し、名前を「Paddle」に変更します。
そしてBox Colliderを削除し、Physics ShapeとPhysics BodyとConvertToEntityをAdd Componentします。
そして、Physics BodyのGravity Factorを0に設定します。
Paddle コンポーネント
Paddleのタグコンポーネントを作ります。
PaddleComponent.csusing Unity.Entities; using System; [Serializable] public struct Paddle : IComponentData { }Force コンポーネント
Paddleに対して左右に力をかけることによってPaddleを移動させようと思います。
そのPaddleにかける力の「方向」と「強さ」をそれぞれ管理するために次のスクリプトを作成します。
ForceComponent.csusing System; using Unity.Entities; [Serializable] public struct Force : IComponentData { public float magnitude; public float direction; }PaddleAuthoring
次のスクリプトを作成し、Paddleオブジェクトにアタッチします。
PaddleAuthoring.csusing UnityEngine; using Unity.Entities; [RequiresEntityConversion] public class PaddleAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new Paddle()); dstManager.AddComponentData(entity, new Force{magnitude = 1}); } }Paddleを操作する
MovePaddleSystem
ここでPaddleに力を与えて動かす処理を担当するSystemを作成します。
$ m $ : Paddleの質量
$ \textbf{v} $ : Paddleの速度
$ \textbf{F} $ : Paddleが受ける外力
$ t $ : 時刻とするとニュートンの運動方程式より
\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dtが成立します。
これを基にMovePaddleSystemを実装します。
(関連 : Unity DOTSとUnity Physicsでただ単純に球を動かしてみる - Qiita)MovePaddleSystem.csusing Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; using Unity.Transforms; public class MovePaddleSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { var dt = Time.DeltaTime; var jobHandle = Entities .WithBurst() .WithAll<Paddle>() .ForEach((ref Force force, ref PhysicsVelocity physicsVelocity, ref Translation position, ref Rotation rotation, in PhysicsMass physicsMass) => { physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt; }) .Schedule(inputDeps); return jobHandle; } }解説
\textbf{v}(t + dt) = \textbf{v}(t) + \frac{1}{m}\textbf{F}dtの式に相当するのが
physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;です。
$\frac{1}{m}$ は
physicsMass.InverseMass
で取得できます。dt はあらかじめ
var dt = Time.DeltaTime;のように
Time.DeltaTime
で取得してキャッシュしています。これをもしキャッシュせずに、ForEach関数内でいきなり使い、
// エラーになる physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt;のように書いてしまうと、
error DC0002: Entities.ForEach Lambda expression invokes 'get_Time' on a ComponentSystemBase which is a reference type. This is only allowed with .WithoutBurst() and .Run().というエラーが出てしまうので注意です。
さて、ここで一度実行して挙動を確認してみます。
あれ?なんか飛んでいってしまいましたね...これは恐らくPaddleとWallが衝突した時に、Paddleに余計な力が加わって位置や姿勢が微妙にズレていってしまうせいです。
なのでMovePaddleSystemを次のように修正します。
MovePaddleSystem(修正版)
MovePaddleSystem.csusing Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; using Unity.Transforms; public class MovePaddleSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { var dt = Time.DeltaTime; var jobHandle = Entities .WithBurst() .WithAll<Paddle>() .ForEach((ref Force force, ref PhysicsVelocity physicsVelocity, ref Translation position, ref Rotation rotation, in PhysicsMass physicsMass) => { physicsVelocity.Linear += physicsMass.InverseMass * force.direction * force.magnitude * dt; position.Value.y = 0; position.Value.z = -4; rotation.Value = quaternion.identity; }) .Schedule(inputDeps); return jobHandle; } }新たに
position.Value.y = 0; position.Value.z = -4; rotation.Value = quaternion.identity;を追加しました。これによりPaddleの位置と姿勢が改善されました。
(しかし、EntityDebuggerでよく調べてみると、RotationもTranslationも何故か若干ズレてしまっています。申し訳ありませんが理由は分かりません。原因究明中です。ご存知の方がいらっしゃったら教えていただけると嬉しいです。)
Ballを作る
Sphereの3D Objectを作成し、名前を「Ball」に変更します。
Positionを(0, 0, 0)、Scaleを(0.5, 0.5, 0.5)に設定します。
Sphere Colliderを削除し、Physics ShapeとPhysics BodyとConvertToEntityをAdd Componentします。Physics ShapeのShape TypeをSphereに変更します。
Frictionを0, Minimumに変更します。
Restitutionを1, Maximumに変更します。そしてPhysics BodyのPhysics Massを0.001に、Linear Dumpingを0に、Angular Dumpingを0に、Gravity Factorを0に、Initial Linear Velocityを(0, 0, -3)に設定します。
(ここで本当はPhysicsMassを0にしたいのですが、最小値が0.001なので0.001に設定しています。)一度実行してみます。
なんだか上手くいっているような感じがします。
しかし、このBall、Paddleとの衝突の度に少しずつ減速しており、最終的に止まってしまいます。
また、Paddleの端をBallにぶつけると一気に加速してしまいます。
なので、Ballの速さを一定に保つ処理を実装することにします。
Ballの速さを一定にする
Ball コンポーネント
Ballのタグコンポーネントを作ります。
BallComponent.csusing System; using Unity.Entities; [Serializable] public struct Ball : IComponentData { }BallSpeed コンポーネント
Ballの速さを管理します。
BallSpeedComponent.csusing System; using Unity.Entities; [Serializable] public struct BallSpeed : IComponentData { public float value; }BallAuthoring
次のようなスクリプトを作成し、Ballオブジェクトにアタッチします。
BallAuthoring.csusing System; using Unity.Entities; using Unity.Mathematics; using Unity.Physics.Authoring; using UnityEngine; [RequiresEntityConversion] public class BallAuthoring : MonoBehaviour, IConvertGameObjectToEntity { private float initialSpeed; private void Awake() { initialSpeed = math.length(this.GetComponent<PhysicsBodyAuthoring>().InitialLinearVelocity); } public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new Ball()); dstManager.AddComponentData(entity, new BallSpeed{value = initialSpeed}); } }解説
initialSpeed = math.length(GetComponent<PhysicsBodyAuthoring>().InitialLinearVelocity);ここではまず
GetComponent<PhysicsBodyAuthoring>().InitialLinearVelocity
でPhysicsBodyのInitialLinearVelocityに設定したベクトル(今の場合は(0, 0, -3))を取得し、そのベクトルの絶対値をmath.length()
で取得してinitialSpeed
に代入しています。SustainBallSpeedSystem
Ballの速さを維持する処理を担当します。
SustainBallSpeedSystem.csusing Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Physics; public class SustainBallSpeedSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { var jobHandle = Entities .WithBurst() .WithAll<Ball>() .ForEach((ref PhysicsVelocity physicsVelocity, in BallSpeed ballSpeed) => { physicsVelocity.Linear = ballSpeed.value * math.normalize(physicsVelocity.Linear); }) .Schedule(inputDeps); return jobHandle; } }Blockを作る
Cubeの3D Objectを作成し、名前を「Block」に変更します。
Positionを(0, 0, 3.5), Scaleを(1, 0.5, 0.5)くらいに設定します。
Box Colliderを削除し、Physics Shape, Physics Body, ConvertToEntityをAdd Componentします。
そしてPhysics BodyのMotion TypeをStaticに変更し、
Physics Shapeの Advanced > Raises Collision Events にチェックを入れます。BallとBlockの接触を判定し、Blockを削除する
Block コンポーネント
Blockを識別するためのタグコンポーネントです。
BlockComponent.csusing System; using Unity.Entities; [Serializable] public struct Block : IComponentData { }BlockAuthoring
次のスクリプトを作成し、Blockオブジェクトにアタッチします。
BlockAuthoring.csusing Unity.Entities; using UnityEngine; [RequiresEntityConversion] public class BlockAuthoring : MonoBehaviour, IConvertGameObjectToEntity { public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem) { dstManager.AddComponentData(entity, new Block()); } }DestroyBlockSystem
BallとBlockが衝突した時、そのBlockを削除する、という処理を実装します。
DestroyBlockSystem.csusing Unity.Burst; using Unity.Entities; using Unity.Jobs; using Unity.Physics; using Unity.Physics.Systems; using Unity.Collections; [UpdateAfter(typeof(EndFramePhysicsSystem))] public class DestroyBlockSystem : JobComponentSystem { private BuildPhysicsWorld _buildPhysicsWorldSystem; private StepPhysicsWorld _stepPhysicsWorldSystem; private EntityCommandBufferSystem _bufferSystem; protected override void OnCreate() { _buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>(); _stepPhysicsWorldSystem = World.GetOrCreateSystem<StepPhysicsWorld>(); _bufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>(); } [BurstCompile] private struct DestroyBlockJob : ICollisionEventsJob { [ReadOnly] public ComponentDataFromEntity<Block> Block; [ReadOnly] public ComponentDataFromEntity<Ball> Ball; public EntityCommandBuffer CommandBuffer; public void Execute(CollisionEvent collisionEvent) { var entityA = collisionEvent.Entities.EntityA; var entityB = collisionEvent.Entities.EntityB; var isEntityABlock = Block.Exists(entityA); var isEntityBBlock = Block.Exists(entityB); var isEntityABall = Ball.Exists(entityA); var isEntityBBall = Ball.Exists(entityB); if (!isEntityABlock && !isEntityBBlock) return; if (!isEntityABall && !isEntityBBall) return; var blockEntity = isEntityABlock ? entityA : entityB; CommandBuffer.DestroyEntity(blockEntity); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var jobHandle = new DestroyBlockJob { Block = GetComponentDataFromEntity<Block>(true), Ball = GetComponentDataFromEntity<Ball>(true), CommandBuffer = _bufferSystem.CreateCommandBuffer(), }.Schedule(_stepPhysicsWorldSystem.Simulation, ref _buildPhysicsWorldSystem.PhysicsWorld, inputDeps); _bufferSystem.AddJobHandleForProducer(jobHandle); return jobHandle; } }解説
衝突時に何らかの処理を行う時は、
ICollisionEventsJob
を実装した構造体を定義します。すると、Execute()
関数の引数に「どの2つのEntityが衝突しているか」という情報が格納されるので、その情報を使って関数内に具体的な処理を書きます。衝突した2つのEntityは、
var entityA = collisionEvent.Entities.EntityA; var entityB = collisionEvent.Entities.EntityB;のようにそれぞれ取得します。
しかし今、entityA
とentityB
は必ずしもBlockやBallとは限りません。なので、
var isEntityABlock = Block.Exists(entityA); var isEntityBBlock = Block.Exists(entityB); var isEntityABall = Ball.Exists(entityA); var isEntityBBall = Ball.Exists(entityB);のように、それぞれのEntityがBlockやBallなのか、という真偽値を取得します。
そして
if (!isEntityABlock && !isEntityBBlock) return; if (!isEntityABall && !isEntityBBall) return;により、衝突しているEntityがBlockでもBallでもない場合はreturnします。
そして最後に
var blockEntity = isEntityABlock ? entityA : entityB; CommandBuffer.DestroyEntity(blockEntity);で「Blockを削除しなさい」という命令をMain Threadへ送っています。
Entityを削除する処理はMain Thread上でしか行えないため、「Blockを削除する」というタスクの発行のみをWorker Threadで行い、後で実際にMain Threadで削除する、ということをEntityCommandBufferSystemを使って行っています。
詳しくは次のサイトをご参照下さい。【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ
また、EntityCommandBufferSystemを使う場合は、OnUpdate()内に
_bufferSystem.AddJobHandleForProducer(jobHandle);のように書く必要があります。
ここではJobが完了する前にCommandBuffer中の命令が実行されてしまうのを防ぐために依存関係を設定する役割を担っています。
(参考 : Class EntityCommandBufferSystem | Package Manager UI website)完成
Blockをしれっと複製して適当に並べて完成です。
参考文献
- 【Unity】 ECS まとめ(前編バー
- 【Unity】 ECS まとめ(後編) - エフアンダーバー
- 【Unity】 ECSへ 思考の移行ガイド - エフアンダーバー
- 【Unity】Unity 2018のEntity Component System(通称ECS)について(1) - テラシュールブログ
- たのしいDOTS〜初級から上級まで〜 - Unite Tokyo 2019
- 大量のオブジェクトを含む広いステージでも大丈夫、そうDOTSならね - Unite Tokyo 2019
- 【Unity】Scene上に構築したステージを、Entity群に変換してECSで利用可能にする「SubScene」 - テラシュールブログ
- 【Unity 入門】【チュートリアル】ブロック崩しを作る 1. 壁、パドル、ボールの作成 - コガネブログ
- 【Unity】C# Job SystemからECSのEntityやComponentDataを追加・削除・変更する指示を出す - テラシュールブログ
関連
- 投稿日:2019-12-09T00:14:50+09:00
ARFoundationとARKit3で光学迷彩的エフェクト
はじめに
こんにちわ、北千住デザインと申します。フリーランスとしてUnityでARアプリを作ってます。
先日、iOS13と共にARKit3が公開されました。そのひとつにピープルオクルージョン(他ではセグメンテーションと呼ばれることも多いです)という機能があります。ARKitではオクルージョンのために使うことを想定しているようですが、画像エフェクトにも使えます。私はこの機能を使って、フィルターアプリMEISAIを制作しています。
そのアプリ内のエフェクトの一つ(仮に光学迷彩エフェクトと呼んでます)の作り方を解説します。やり方は比較的簡単ですが、見たことのないエフェクトになってると思うので是非ごらんください
A new AR effects app "MEISAI" was released☺️
— Kitasenju Design (@kitasenjudesign) September 28, 2019
こんな光学迷彩みたいなエフェクトも作れます☺️
? https://t.co/CebLlcEGdF
This app works only on iPhone XR/XS/11+iOS13. but it’s free.#MEISAI #AR #ARKit #ARFoundation #MadeWithUnity #iOS13 pic.twitter.com/NdWskWsGKgUnityでARKit3を使う
Unityで手っ取り早くARKitを利用するには、ARFoundationというARKitやARCoreのラッパーみたいなやつを使う必要があります。こちらにサンプルが公開 されてるので、これのHumanSegmentationImagesというシーンにコードを追加していきます。
今回のサンプルのリポジトリはこちらです。
https://github.com/kitasenjudesign/ARFoundationMeisaiDemo
作ったシーンやコードは主にAssets/MEISAI/にアップしてます。人物のシルエットを赤くする
ポストエフェクトによって人物を赤くしてみます。
まずポストエフェクト用に、人物のシルエット(humanStencilTexture)をシェーダーに渡します。
PostEffect.csusing UnityEngine; using UnityEngine.XR.ARFoundation; public class PostEffect : MonoBehaviour { [SerializeField] private Shader _shader; [SerializeField] private AROcclusionManager occlusionManager; private Material _material; void Awake() { _material = new Material(_shader); } void OnRenderImage(RenderTexture source, RenderTexture destination) { if(occlusionManager!=null){ _material.SetTexture("_StencilTex",occlusionManager.humanStencilTexture); } Graphics.Blit(source, destination, _material); } }次にhumanStencilTextureを使って、シルエットを塗り潰します。しかしhumanStencilTextureを利用する際、一つ問題があります。humanStencilTextureとカメラから得られるテクスチャのサイズや角度が違うため、UVを補正する必要があるんです。
以下の例は、スマホを縦持ちし(portrait)、カメラからの映像のテクスチャが1920x1440のとき、正しくなるように補正しています。
StencilSample.shaderShader "StencilSample" { Properties { _MainTex ("_MainTex", 2D) = "white" {} _StencilTex ("_StencilTex", 2D) = "white" {} } 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; sampler2D _StencilTex; float2 GetStencilUV( float2 uv ){ float2 stencilUV = float2( 1-uv.y, 1-uv.x ); float camTexWidth = 1920; float camTexHeight = 1440; float aspect = (camTexWidth/camTexHeight) / (_ScreenParams.y/_ScreenParams.x); stencilUV.y = stencilUV.y * aspect + (1-aspect)/2; return stencilUV; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); fixed4 stencil = tex2D(_StencilTex, GetStencilUV(i.uv));//UVを補正 return lerp( col, fixed4(1,0,0,1), stencil.r); } ENDCG } } }ノイズ関数とカメラの色を利用し光学迷彩を作る
前述の例をもとに、シェーダーでエフェクトとアニメーションを加えます。
金属や水なんかは鏡面反射や屈折なんかによってその材質感が現れます。ここでは擬似的に鏡面反射や屈折のようなエフェクトを作ることで、光学迷彩っぽいエフェクトを作っています。
これはAfterEffectでいうディスプレイスメントマップフィルタというテクニックで、テクスチャの色に従ってUVを変位させています。ここでは、カメラからのテクスチャの色とノイズ関数を使うことによって、人物の特徴をなんとなく保持しつつ、金属のような水のような質感を作り出します。ノイズ関数はこちらを使わせていただきました。
MEISAI.shaderShader "MEISAI" { Properties { _MainTex ("_MainTex", 2D) = "white" {} _StencilTex ("_StencilTex", 2D) = "white" {} } SubShader { // No culling or depth Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "./noise/SimplexNoise3D.hlsl" 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; sampler2D _StencilTex; float2 GetStencilUV( float2 uv ){ float2 stencilUV = float2( 1-uv.y, 1-uv.x ); float camTexWidth = 1920; float camTexHeight = 1440; float aspect = (camTexWidth/camTexHeight) / (_ScreenParams.y/_ScreenParams.x); stencilUV.y = stencilUV.y * aspect + (1-aspect)/2; return stencilUV; } fixed4 frag (v2f i) : SV_Target { fixed4 camCol = tex2D(_MainTex, i.uv); //ノイズ関数と色を基に変異させる。数値は適当 float2 displacedUV = float2( 0.5 * snoise(float3(i.uv.x+camCol.r*2.0,i.uv.y+camCol.g*2.0, _Time.y*0.3)), 0.5 * snoise(float3(i.uv.x+camCol.g*2.0,i.uv.y+camCol.b*2.0, _Time.y*0.4)) ); displacedUV = frac( i.uv + displacedUV ); fixed4 displacedCol = tex2D(_MainTex, displacedUV); fixed4 stencil = tex2D(_StencilTex, GetStencilUV(i.uv)); return lerp( camCol, displacedCol, stencil.r); } ENDCG } } }おわりに
セグメンテーションとシェーダーを組み合わせることで、様々なエフェクトを作ることができます。最初、試しにシェーダーでアニメーションさせてみたとき、思った以上に面白くてびっくりしたので、ぜひやってみてください。
最後にセグメンテーションを用いた応用例をもう一つ紹介しておくと、人物からパーティクルが出るようなエフェクトも作ることができます。このやり方はまた機会があれば書きたいと思います。このエフェクトはMEISAIでも試せますので使ってみてください
ARISE大盛況で東大の稲見教授とEnhance水口さんのラストセッションも始まった!
— KAJI / MESON CEO ? (@kajikent) November 30, 2019
(フィルターはPORTALの演出周りの実装をしてくれていて今日登壇してくれた北千住さんのMEISAIアプリ)#ARISE pic.twitter.com/pPgrG14QrNmetome x Kitasenju Design & Kaiware Style
— MUROI(ムロイ)@岩本町芸能社er (@ion_muroi) December 4, 2019
ほんまチャネルきてよかった pic.twitter.com/HfpBFAN64P