20191209のUnityに関する記事は16件です。

【Unity】無料アセット「CSV Serialize」を使ってスプレッドシートでマスタデータ管理

プロトタイプ作成に便利そうなアセット「CSV Serializer」の使い方の紹介

2019-12-09.png

簡単に説明すると。CSVを簡単にC#クラスにパースしてくれるアセット
スプレッドシートもCSV形式でダウンロードすればすごく簡単にUnityに持ってこれる

1.データクラスを用意

てきとうにスプレッドシートで管理したいデータクラスを作る

public class Enemy
{
    public int id;
    public string name;
    public int hitPoint;
    public int power;
    public int defence;
}

2.対応するスプレッドシートのシートを用意

てきとうにデータの中身も用意する
2019-12-09 (1).png

これをCSV形式でダウンロードできるように公開設定する
・「ファイル」 →「ウェブに公開」
・シートの指定をさっき作ったシートにし、形式を「カンマ区切り (.csv)」にして公開。表示されるURLをコピー

2019-12-09 (3).png

2019-12-09 (2).png

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とかにしておけば開発中はスプレッドシート、製品版はアプリ内に埋め込み、とかも簡単にできる。便利!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでスクリプトのビルドが走らなくなった時はAuto Refreshを確認すること

ある日突然、スクリプトを保存してもビルドされなくなった

いつものようにスクリプトを編集、保存、Unityエディタに切り替えると走るはずの自動ビルドが走らない。
編集したスクリプトをProjectでクリックしてInspectorを確認しても、古いまま。
仕方ないので右クリックしてReimportをするとビルドが走る。

不便なので色々なキーワード、「Unity」「ビルド」「自動」「Import」などで検索してみても、まぁ大体Jenkinsとかの記事が出てくるばかりで。。

数日不便なまま過ごした挙句、結局詳しい方に教えていただいた。

原因はAuto Refreshのチェックボックス

原因は、PreferencesのGeneralの先頭にある、Auto Refresh のチェックボックスが外れていた事。

autorefresh.png

まったく覚えはないけれど、多分Preferencesにフォーカスを移そうとした時に、クリックしてチェックを外してしまったのだと思われる。

同じような事態に陥ってしまった人のために、、、記録として残しておきます。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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についているコンポーネントをみてみると・・・
image.png
なるほど、RigidbodyとOVRGrabbableスクリプトをつけてやるだけで(実質コード一行も書かなくても)手でつかめるんです!私がすごいんじゃなくてOculusがすごいんだけど。

_01_3は、

Oculus Questのコントローラーを使ったシーン。これだと握っているコントローラーと見た目が同じになるので違和感がなくなります。でも残念ながら、こいつにはボタンを押したときのアニメーションがついてないんだよねえ・・・

_01_4は、

左右のコントローラーの場所に細長くしたCubeをつけたシーン。もしBeat Saberのようなゲームを作りたくなったら、このシーンをベースにしていただければ。サンプルだからそっけないCube、というかただの棒だけど、アセットストアでかっちょいい剣のモデルを買って、このCubeの角度や大きさにあわせて配置してくれれば、なんかそれっぽくなると思いませんか?

 でも、実はそれだけではないんです。

これらのシーンに配置されているOVRCamerarigは、すでにコントローラー付きでプレファブにしてあるので、それをそのままご自分のシーンにいれて使い回すことができます。
OVRCameraRigWithController
OVRCameraRigWithQuestController

 しかも!

そのプレファブには、すでにOVRPhysicRaycasterスクリプトがついているんです。
image.png
このあたりの話は次回もう少し詳しくやるつもりなのですが、このスクリプトは、コントローラーからレーザーポインターをだしてゲームオブジェクトを操作したいときに必要なやつ。つまりこのプレファブは、単にコントローラーが付いているという見た目だけでなく、機能的にも、このままで便利に使える優れもの。
ぜひ自分の心向けに使ってください。

 今日はここまで

今日はシーン名が01_xx というシリーズの紹介をさせていただきました。非常に基本的なシーンだけど、ここで使っているプレファブが便利設定になっているので使いまわしてほしい、というのも伝えたかったことです。実際、このサンプルの02_以降のシリーズはこれらのプレファブをつかってできています。

 ところで、あなた誰?、って方

宣伝っぽくて恐縮ですが、VRプロフェッショナルアカデミーという、VRコンテンツの作り方を教える学校で、初心者向けコースの講師をやっています。このBasicsも、生徒向けの教材の一環として作ったものなのですが、自分の生徒以外にも初心者の方には広く使っていただきたいとおもっています。
VRコンテンツ作ってみたい!っておもった方はこちらも覗いてみてください。
https://vracademy.jp/vrBeginner.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

遺伝的アルゴリズム的なものを作ってみたお話

はじめに

最近AI/アルゴリズムの講演を聞きました。そこで、遺伝的アルゴリズムに興味を持ったので「作ってみたい!」と思い、作成したので、何かの参考になればと思いご紹介します。

遺伝的アルゴリズムとは~Wikipedia~

実際の挙動

movie.gif

movie1.gif


movie2.gif

これらは毎フレームランダムに動かしているだけですが、とても人工知能ぽくみえます。

実践

ソースコードはこちら
※3D空間上でXZ平面を走っているのを上から撮影しています

car.cs
float 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を

movie3.gif

20体用意して、

movie4.gif

100mごとに障害物を置きます。

移動データの記録

それから動いた記録を保存するデータを作成します。

data.cs
[Serializable]
public struct MoveData
{
    public float pos;
    public float rot;
    public MoveData(float _pos, float _rot)
    {
        pos = _pos;
        rot = _rot;
    }
}

これのリストを作ってランダムに動いた結果を保存しておきます。
具体的な保存の値はそれぞれ、上記のrotVeloposVeloです。

全員が壁にぶつかったら集計をします。

その際にどのObjectが一番進んだかを確かめて次の世代を生成します。

遺伝子の受け継ぎ

marry.cs
for (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.cs
if(突然変異する)
{
  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 AI/Algo DEMO

終わりに

講演とかYoutubeの動画を見ながらこんな感じで作るのかなーって作りきりました。
いいお勉強になって楽しかった。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

長いTextの終わりに "..." をつけ、かつ改行させない方法

Textの幅が枠内に収まりきらないとき、私たちのチームでは、以下の記事の(4) 余談の部分を参考に、溢れない箇所でカットして、 ... を末尾に付与しています。

[Unity] Textコンポーネントでテキストを切り捨てずに全文表示させる方法

スクリーンショット 2019-12-06 16.03.37.png

以下に一部引用させていただきます。

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行目の最後の文字がリミットと判断されるようです)

スクリーンショット 2019-12-06 18.02.58.png

かといって、TextのHorizontal OverflowをOverflowにすると、今度は全て表示可能とみなされ、...を付与されず全部そのまま出てしまいます…。

スクリーンショット 2019-12-06 18.03.08.png

(ただし、この問題は英語のみでは起きないようです。)

スクリーンショット 2019-12-06 17.58.29.png

そこで、私たちがとった対策は、半角スペースをnbsp(non-breaking space)に置き換えることです。

このnbsp、htmlなどでは&nbsp;としてご存知の方もいるかと思いますが、文字コードとしても存在します(\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()しているだけです。

これで、半角スペースが含まれていても単語で区切らずに ... を付与することができました。

スクリーンショット 2019-12-06 18.03.23.png

英語では問題が起きないところを見るとUnityの処理の不具合のような気もしますが、ともかくこれで想定どおりの挙動になりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity2019.2から起動するエディタでコード補完(オートコンプリート)を有効にする

状況

UnityのAssetsから直接.csファイルを開いた際、エディタ上でオートコンプリートが効かず、ものすごく困りました。

キャプチャ.PNG

MonoBehaviorクラスが認識されず、VSCode/VisualStudio2019の両方でオートコンプリートが無効。なんでや。

環境

以下の通りです。

Unity
2019.2.13f

VSCode
10.40.2

VisualStudio2019
Communityのちょっと前のやつ

解決方法

VisualStudioの場合

【Unity】Visual Studioのインテリセンス(自動補完や候補予測)が効かない場合の対処法

UnityでVisualStudio2017のIntelliSenseが動作しなくなる問題を解決する

リンク先の通りにやったらできた!圧倒的感謝。

自分の環境の場合、VS2019のバージョンを16.4.0に上げただけでいけた。

VSCodeの場合

今日からUnity + Visual Studio Codeを用いた快適な開発生活(随時更新中)

こちらに記載のあるアセットを入れてみましたが、解決に至らず。

MonoBehaviour Snippetsという拡張機能を追加してみたら、それっぽくはなりました。
相変わらずMonoBehaviorクラスは認識されないので、根本的な解決にはなってない。

解決でき次第、追記したい。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】inspectorでクラスを設定する。

クラスをInspectorに表示する。

[System.Serializable]
public class SkillEntity
{
    public Skill.Target target;
    public Skill.Timing timing;
    public Skill.Type type;
    public int lineTimeSpan;
    public List<int> argument;
}

・「System.Serializable」属性をクラスに設定する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityで3Dモデルを画像書き出しする

はじめに

アドカレ10日目の記事になります。

3Dゲーム開発を行なっていると、3Dモデルを使ったバナー作成などをクリエイターさんが扱う事があります。
今回はそういった素材用の画像書き出し処理をエンジニアがサポートできるような事を書いていきます。

環境

Unityバージョン

Unity: 2018.4.13f1

Scene構成

3D(モデル)の表示をRenderTextureで2D(UI)上に表示しています。
3Dカメラ表示
スクリーンショット 2019-12-09 1.58.01.png
2Dカメラ表示
スクリーンショット 2019-12-09 2.02.37.png

やりたい事

この画面から
スクリーンショット 2019-12-09 1.49.18.png
バックグリーンのものと透過画像を書き出します
capture.pngcapture.png

画像書き出し方法

Unityからの画像書き出し方法ですが、ここではスクリプト(RenderTextureの写し)を使用した場合とUnityRecorderを使用する場合の2パターンを紹介します。

1.スクリプトで画像書き出し

c#Capture.cs
using 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のサイズで書き出されます。
スクリーンショット 2019-12-09 3.08.01.pngスクリーンショット 2019-12-09 3.08.20.png

ちなみに、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/583219cb0366b758a7fe

2.Unity Recorderを使用して画像書き出し

Editorを使用できる場合、Unity2018からPackage ManagerにあるUnity Recorderを使用することで上記画像を簡単に保存する事ができます。(Install方法についてははぶきます)

手順

  1. Add New RecordersからImage Sequenceを選択します。
  2. Captureの対象をGame ViewからRnder Texture Assetに変更します。
  3. RenderTextureに3Dカメラで設定しているRenderTextureを設定します。
  4. FormatJPEGからPNGに変更します(Capture Alphaにチェックを入れなくても透過書き出しできた)。
  5. Game Sceneを実行し3DカメラのSolid Colorの値を書き出したい背景色に変更する。
  6. START RECORDINGで指定したPathに保存されます。

以下の状態になっていれば大丈夫だと思います。
スクリーンショット 2019-12-09 10.48.16.png

キャプチャと書き出し結果
Recorder.gif
スクリーンショット 2019-12-09 10.55.22.png

参考サイト様
http://bibinbaleo.hatenablog.com/entry/2018/10/08/160737#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88

おまけ(スクリプト)

右下にロゴを追加する

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処理内~~

スクリーンショット 2019-12-09 11.12.54.pngcapture.png

画像を縮小する

パターン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();

スクリーンショット 2019-12-09 11.31.26.pngcapture.png

最終コード

Capture.cs
using 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Realsenseの赤外線カメラを利用して簡易モーショントラッキングを行う

概要

この記事は 3D Sensor Advent Calendar 2019 の9日目の記事になります。
OptiTrack等のモーションキャプチャシステムが行っている赤外線カメラでマーカーをトラッキングして位置を追跡する手法をRealsenseの赤外線カメラと再帰性反射テープを使って作ってみたので紹介します。

出来る事

再帰性反射テープを張り付けた物体の位置をトラッキング出来ます。90FPSでトラッキング出来るので素早い動きもトラッキング出来ます。これらの処理を全てUnity上で行います。

以下のような事ができます。

必要機材

合計5~6万ほどで機材一式が揃います。

検証に利用したソフトウェア、ライブラリ

  • 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.PNG

RsDeviceからTextureを受け取るのはRsStreamTextureRendererになります。ちなみにStreamIndexが1の場合がReanselseの右のIRカメラ、2が左のIRカメラとなります。
rsStreamRender.PNG

2. OpenCVでBlob detect

OpenCVforUnityのSimpleBlobExampleシーンを参考にしました。下記に、BlobDetectでBlobのピクセル座標を取得する所までのサンプルを載せます。詳細はコメントに書きました。

BlobDetectorsample.cs
using 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次元座標に変換

下記にコードを書きました。処理の流れはコメントを参照してください。

Blob3DSample.cs
using 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.cs
public 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眼の距離を物理的に離すのが一番簡単です。

まとめ

Realsenseでモーショントラッキングする仕組みをDIYしました。
明日12/10(火)はxbarusuiさんで「初心者が Unity で始める Azure Kinect」です。お楽しみに!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

音声解析リップシンクミドルウェア「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母音の組み合わせ情報の時間推移を示します。
日本語五母音の組み合わせ情報.png
図を見てみると、「あ」「い」「う」については比較的精度高く推定できていそうですが、この音声では「え」と「お」はほとんど検出されなかったようで、子音や前後の音との組み合わせによっては推定が難しいケースもあるようです。そもそも、フォルマント的にも各母音のフォルマントの分布は完全に分離されているという訳ではなく、一部が重複した状態で分布していることも、推定を誤ってしまう原因として考えられます。

次に、縦横形状の組み合わせ情報の時間推移を見てみます。
縦横形状の組み合わせ情報(口の高さ).png
縦横形状の組み合わせ情報(口の幅).png
口の高さの推移の図を見てみると、口を縦に大きく開く「a」や「o」の音で極大となっており、全体的に良く追従できているのが分かります。さらに、図の赤丸で示した「だ」と「ら」の間では、一瞬口を閉じている状態のデータを返しており、唇音ではないが間で口を閉じる発音にも対応できていることが伺えます。

最後に、CriLipsAtomAnalyzerクラスのGetRmsメソッドから取得した音圧の強さを表すRMS(Root Means Square)値の時間推移です。
RMS値(≒音圧の高さ)の推移.png
こちらのデータだけを使ってリップシンクをさせるのは難しそうですが、口形状情報と組み合わせて使うと音量による口の開き具合の影響度を大きくしたい場合などに役立つかもしれないです。CriLipsAtomAnalyzerクラスのGetVolumeメソッドでも音量(dB値)が取得できるようなので、今後そちらも検証してみたいと思います。

まとめ

自然なリップシンクを自前で実装しようとすると、実装も難しく計算負荷も高くなってしまいがちですが、『CRI ADX2』による再生機構と『CRI ADX LipSync』が提供する解析機構を組み合わせて使うことで手軽に品質の高いリップシンクを実現することができそうだということが分かりました。
実機でのプロファイリングで問題がなければ、プロジェクトでの導入も検討していきたいです。

参考

キャラクターをより魅力的に!ゲーム向けリップシンクミドルウェア CRIWARE公式ページより
音声解析リップシンクミドルウェアCRI ADX LipSync

明日は @kojima_akira さんの記事です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

8頂点でサイリウムを綺麗に表現する

この記事では以前にちょっとした実験で実装してみた、8頂点でサイリウムを表現する方法をご紹介します。

Unityプロジェクトはこちらから参照できます!

https://github.com/kaneta1992/EightVerticesPsyllium

以下のgif動画はこの記事で紹介する8頂点を利用した方法(ON)と、オーソドックスな方法(OFF)を比較したものです。結構よい感じになっているのではないでしょうか!(サンプルのため動きのクォリティ等はご了承ください :fearful:

image1.gif

まずはよくある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から上方向のベクトルとサイリウムの位置を取得して、それを元に新しい姿勢の計算を行い、頂点のワールド変換を行います。

image8.png

これだけでもそれなりの品質になりますが、視線とサイリウムが平行になるにつれて、ペラペラな板ポリであることがバレてしまいますね。

次の方法で、これをできるだけ目立たないように改善してみます。

8頂点にしてペラ感を改善する

ここからは最初に書いた通り、4頂点と少しの工夫を追加して品質を向上します。

頂点を追加する

4頂点では表現力に限界があるので、サイリウムの上下の半円の場所に頂点を追加して、8頂点にします。

image9.png

頂点にデータを埋め込む

変形で使用するため、先ほど追加した頂点に半円部分の半径を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),
};

メッシュを生成しているスクリプトの全文はこちらです。

image2.png

カメラ位置を利用してメッシュを変形する

この段階ではまだペラペラなので、追加した頂点をビルボードのように、カメラの位置を見て、常に真正面に見えるように変形します。

image7.png

さらに視点を固定したまま横から見ると以下のようになってます。

image6.png

これを実現するために、上で作った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頂点にしてシェーダーを二行加えただけで品質がかなり向上しました!
image3.png

シェーダーの全文はこちらです。

さいごに

今回実験で実装してみましたが、思いのほか綺麗に見えたので記事にしてみました。

ちょっとした工夫で綺麗に見せることができるものがまだまだあると思うので今後も目を凝らして観察していきたいともいます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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へ行きます)
スクリーンショット 2019-12-08 19.28.00.png

シーン2: セーブするプレイヤーの情報を入力。
スクリーンショット 2019-12-08 19.28.37.png

シーン3: ゲームシーン。セーブデータが正しく保存されているか確かめるためにセーブデータをロードしてプレイヤーに反映します。
スクリーンショット 2019-12-08 19.28.46.png

全部を紹介すると長くなるのでセーブ、ロードに関するところをピックアップして以下に紹介します。

シーン2

シーン1はシーン遷移のみなので飛ばします。

まずセーブするプレイヤーのデータ

SavePlayerData.cs
[System.Serializable]
public class SavePlayerData
{
    public string name;
    public int age;
    public string color;
}

セーブするクラスにはSystem.Serializableと言う属性をつけることでシリアル化を可能にします。
Monobehaviourを継承しているとシリアル化できないの注意しましょう。

次にユーザーの入力をもとにセーブデータを作りシリアル化する箇所です。

SaveManager.cs
using 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.cs
public 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
オブジェクトのシリアル化

コメント

以下の観点でコメントいただけると嬉しく思います!

  • 間違い、不備
  • 「こういう状況では、このセーブ/ロード方法が良いよ!」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】「機動戦士ガンダムVS.」シリーズっぽいカメラの動きを作ってみる

はじめに

本記事ではUnityで「機動戦士ガンダムVS.」シリーズにあるような、操作キャラクターを視界に入れつつ、ターゲットに向き続けるTPSカメラを作ってみます。
近い動きで「ディシディア ファイナルファンタジー」や「Fate/Grand Order Arcade」のようなカメラにも応用できるんじゃないかと思います。

完成イメージ

outcome.gif

デモプロジェクト

GitHubにアップロードしています。
unity-tps-lock-on-camera

位置と向きのロジック

カメラの動きを作るときは先に位置を考え、その後に向きを考えるのが進めやすいと思います。

位置

まずカメラの位置ですが、下図のようにターゲットと操作キャラを結んだ線上、操作キャラ後方の少し上辺りにいる感じがしますね。

camera_image01.png

camera_image02.png

なので、操作キャラ→ターゲットのベクトル$\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.cs
using 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);
    }
}

prototype.gif

ターゲット切り替えのロジック

続いてターゲット切り替えの動きを考えます。
最終的にはターゲットとして指定しているtransformを変えればいいわけですが、単純に変えるとカメラが位置と向きをがっつりワープすることになるので、プレイヤーは切り替わり前後で混乱してしまいますよね。滑らかに繋ぎたいところです。

ターゲット切り替え

処理の流れを考えます。
まず、元々見ていたターゲットは、切り替わり動作中も含めて無視としたいので、切り替え開始の瞬間の位置$P_{old}$だけ覚えておけばよさそうです。新しく見るターゲットは切り替わり動作中も含めてその位置$P_{new}$を追従し続けたいです。
なので$P_{old}$は固定位置、$P_{new}$はTransformのpositionとして、$P_{old}$から$P_{new}$へ補間しつつ一定時間かけて移動すればよさそうです。
(見る位置を滑らかに変えることで、カメラの位置と向きもスムーズに変化するよねという考え方です。)

camera_image03.png

位置の補間にはVector3.Lerpのメソッドが使えます。(好みでイージングしてもいいかもしれません。)
よって、先程のコードにターゲット変更の処理を組み込んだ最終版はこのようになります。

TpsLockOnCamera.cs
using 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);
    }
}

おわりに

このタイプのカメラ挙動はカメラ操作がシンプルになるというメリットがありますね。
拙い説明であることに加え、細かくは再現しきれておらず申し訳ないですが、少しでも何かの参考になれば幸いです。
また、説明の間違いやソースコードの不具合などあるかもしれません。その場合は是非ご指摘いただけますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

フロー

  1. bitrise CLIのインストール
  2. Unityプロジェクト
    • ビルドスクリプトの追加
    • プロジェクト設定
    • ビルドステップの追加
  3. ipaアーカイブステップの追加

内容

1. bitrise CLIのインストール

homebrewでインストールします。
その後、セットアップコマンドでツール群をインストールします。

$ brew update && brew install bitrise
# install log
$ bitrise -v
1.36.0
$ bitrise setup
# setup log

bitriseの処理は 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.md

2. 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/Build

3. 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_idforce_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ビルドができる方法が紹介されてるので、今回の続きとして、ローカル環境ではなくクラウドで実行できるようにしたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)に設定しておきます。

スクリーンショット 2019-12-06 午後3.33.25.png

全ての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 ShapePhysics BodyConvertToEntityをAdd Componentします。
スクリーンショット 2019-12-06 午後7.08.20.png

そして、Physics BodyのGravity Factorを0に設定します。

スクリーンショット 2019-12-07 午前11.09.31.png

Paddle コンポーネント

Paddleのタグコンポーネントを作ります。

PaddleComponent.cs
using Unity.Entities;
using System;

[Serializable]
public struct Paddle : IComponentData
{
}

Force コンポーネント

Paddleに対して左右に力をかけることによってPaddleを移動させようと思います。

そのPaddleにかける力の「方向」と「強さ」をそれぞれ管理するために次のスクリプトを作成します。

ForceComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Force : IComponentData
{
    public float magnitude;
    public float direction;
}

PaddleAuthoring

次のスクリプトを作成し、Paddleオブジェクトにアタッチします。

PaddleAuthoring.cs
using 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.cs
using 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().

というエラーが出てしまうので注意です。


さて、ここで一度実行して挙動を確認してみます。
タイトルなし.gif
あれ?なんか飛んでいってしまいましたね...

これは恐らくPaddleとWallが衝突した時に、Paddleに余計な力が加わって位置や姿勢が微妙にズレていってしまうせいです。

なのでMovePaddleSystemを次のように修正します。

MovePaddleSystem(修正版)

MovePaddleSystem.cs
using 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の位置と姿勢が改善されました。

タイトルなし.gif

(しかし、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に設定しています。)

一度実行してみます。

タイトルなし.gif

なんだか上手くいっているような感じがします。

しかし、このBall、Paddleとの衝突の度に少しずつ減速しており、最終的に止まってしまいます。

また、Paddleの端をBallにぶつけると一気に加速してしまいます。

タイトルなし.gif

なので、Ballの速さを一定に保つ処理を実装することにします。

Ballの速さを一定にする

Ball コンポーネント

Ballのタグコンポーネントを作ります。

BallComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Ball : IComponentData
{
}

BallSpeed コンポーネント

Ballの速さを管理します。

BallSpeedComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct BallSpeed : IComponentData
{
    public float value;
}

BallAuthoring

次のようなスクリプトを作成し、Ballオブジェクトにアタッチします。

BallAuthoring.cs
using 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.cs
using 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;
    }
}

タイトルなし.gif

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 にチェックを入れます。

スクリーンショット 2019-12-08 午後6.49.58.png

スクリーンショット 2019-12-08 午後10.41.19.png

BallとBlockの接触を判定し、Blockを削除する

Block コンポーネント

Blockを識別するためのタグコンポーネントです。

BlockComponent.cs
using System;
using Unity.Entities;

[Serializable]
public struct Block : IComponentData
{
}

BlockAuthoring

次のスクリプトを作成し、Blockオブジェクトにアタッチします。

BlockAuthoring.cs
using 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.cs
using 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;

のようにそれぞれ取得します。
しかし今、entityAentityBは必ずしも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)

タイトルなし.gif

完成

Blockをしれっと複製して適当に並べて完成です。

タイトルなし.gif

参考文献

 関連

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ARFoundationとARKit3で光学迷彩的エフェクト

はじめに

こんにちわ、北千住デザインと申します。フリーランスとしてUnityでARアプリを作ってます。

先日、iOS13と共にARKit3が公開されました。そのひとつにピープルオクルージョン(他ではセグメンテーションと呼ばれることも多いです)という機能があります。ARKitではオクルージョンのために使うことを想定しているようですが、画像エフェクトにも使えます。私はこの機能を使って、フィルターアプリMEISAIを制作しています。

そのアプリ内のエフェクトの一つ(仮に光学迷彩エフェクトと呼んでます)の作り方を解説します。やり方は比較的簡単ですが、見たことのないエフェクトになってると思うので是非ごらんください:bow_tone1:

UnityでARKit3を使う

Unityで手っ取り早くARKitを利用するには、ARFoundationというARKitやARCoreのラッパーみたいなやつを使う必要があります。こちらにサンプルが公開 されてるので、これのHumanSegmentationImagesというシーンにコードを追加していきます。

今回のサンプルのリポジトリはこちらです。
https://github.com/kitasenjudesign/ARFoundationMeisaiDemo
作ったシーンやコードは主にAssets/MEISAI/にアップしてます。

人物のシルエットを赤くする

ポストエフェクトによって人物を赤くしてみます。

IMG_0886b.png

まずポストエフェクト用に、人物のシルエット(humanStencilTexture)をシェーダーに渡します。

PostEffect.cs
using 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.shader
Shader "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
        }
    }
}

ノイズ関数とカメラの色を利用し光学迷彩を作る

前述の例をもとに、シェーダーでエフェクトとアニメーションを加えます。

RPReplay_Final1575685579.gif

金属や水なんかは鏡面反射や屈折なんかによってその材質感が現れます。ここでは擬似的に鏡面反射や屈折のようなエフェクトを作ることで、光学迷彩っぽいエフェクトを作っています。

これはAfterEffectでいうディスプレイスメントマップフィルタというテクニックで、テクスチャの色に従ってUVを変位させています。ここでは、カメラからのテクスチャの色とノイズ関数を使うことによって、人物の特徴をなんとなく保持しつつ、金属のような水のような質感を作り出します。ノイズ関数はこちらを使わせていただきました。

MEISAI.shader
Shader "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でも試せますので使ってみてください:bow_tone1:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む