20191214のUnityに関する記事は17件です。

【Unityアセット冬のアドカレ2019】Low Poly Water GPUを紹介

この記事はUnityアセット冬のアドベントカレンダー 2019 Winter!、15日目の記事になります。

昨日はEilzyさんさんの
「BlackFridayで買ったAssets「Unka the Dragon」と「RTSCamera」について紹介」でした。

概要

本日ご紹介したいアセットはLow Poly Water GPUです。
使い方簡単な低ポリウォーター(GPU)セットです。

Low Poly Water GPU
01.jpg

機能(Google翻訳そのまま):
•GPUウェーブ計算(CPUなし)。
•カスタマイズ可能なウェーブとライティング。
•ショアブレンディング(フォーム)。
•平面反射(VR以外)。
•光吸収。
•影の受信。
•最大4ポイントライト。
•カスタム水メッシュ。
•最適化およびテスト済みモバイル。

とりあえず遊んでみます

先にLow Poly Ultimate PackをUnityに導入し、Low Poly Ultimate Packの水は実行しても動かないまま。
01.gif

一旦島だけSceneに残り、
スクリーンショット 2019-12-14 午後9.12.19.png

Low Poly Water GPU内は4つ違い種類のプレハブがあって、一旦LPWaterPerformanceを入れます。
スクリーンショット 2019-12-14 午後9.16.19.png

プレハブのデフォルトサイズは(x,z)30x30で、小さいです。
スクリーンショット 2019-12-14 午後9.16.52.png

(x,z)100x100を入れて、
スクリーンショット 2019-12-14 午後9.19.20.png

FullHD Viewには満遍になります。
スクリーンショット 2019-12-14 午後9.19.25.png

この状態で実行してみたら、
LPWaterPerformance(gif):
02.gif

ほかの種類も実行

LPWaterFoam(gif):
LPWaterFoam.gif

LPWaterHighQuality(gif):
LPWaterHighQuality.gif

LPWaterShadows(gif):
LPWaterShadows.gif

コンポーネントの使い方法

公式ドキュメントはこちらです。
https://github.com/jolix/Low-Poly-Water/wiki

公式ドキュメントはGoogle翻訳で簡単に読めますので、この記事は主にメイン箇所だけピックアップします。

プロパティ:Grid Type
スクリーンショット 2019-12-14 午後9.19.20.png

  • Hexagonal(六角形):六角形の水平面を生成します。
  • Square(正方形): 正方形の水平面を生成します。
  • Hexagonal LOD(六角形のLOD):LODオプションで六角形の水平面を生成します。
  • Custom(カスタム):カスタムメッシュを使用できるようにします。

プロパティ:Enable Reflection
スクリーンショット 2019-12-14 午後10.38.07.png

  • 平面光の反射を有効にします。

スクリーンショット 2019-12-14 午後10.40.59.png
Enable Reflectionを付けたら、石の反射ができてました(ほかのは黒くて、違いさが区別できない(汗)...)。

プロパティ:Waves
Shaderのところで、波の速度、密度など自由にカスタマイズできます。
スクリーンショット 2019-12-14 午後10.58.05.png

Wavesのデフォルト(gif):
04.gif

他には屈折、ライティング、深度など細かく設定でき、カスタマイズによって湖や川っぽいも作れます。

湖っぽい(gif)
06.gif

川っぽい(gif)
05.gif

最後

あくまで入門ぐらい程度でこの記事を書きました。興味ある方は是非Storeへ見てください。

明日はさとやんさんの「IntroLoopで簡単にイントロ付きループBGMを作る」です。

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

Zenject を使ったプロジェクト運用

最近仕事のプロジェクトで Zenject を使うようになって、個人的に実運用をしていくのに有用そうなワークフローを確立しつつあるので、それを紹介します。基本的に UI をベースにしたゲームを作っているので、そのような書き方が多くなると思いますが、大体は普遍的にゲームに適用できるかと思います。ちなみに DOTS は範囲外とします(DI じゃなくて ECS を使おう!)。

Zenject の前に

Zenject に関する説明はネット上にころころあるので既に知っている方が多いかと思うのですが、必要最低限な前提を記述しておきます。

Zenject は、いわゆる DI(Dependency Injection)フレームワークと呼ばれるもので、DI を行うためのツールになっています。

DI は、オブジェクト指向で言われるSOLIDの「依存性逆転の原則」を実現するための手法です。例えば以下のような、モデルとビューの依存関係があるとします。(簡素化しています)

Screenshot from 2019-12-11 22-31-48.png

モデルがビューを所有し、モデルに変更があった場合ビューを直接呼び出しています。これはビューに変更に非常に弱い形になります。SetHp の引数が変われば破綻しますし、View クラスが削除されても破綻します。これを解消するために、依存性を逆転させて変更への弱さを解消します。

Screenshot from 2019-12-11 22-38-18.png

View クラスへの矢印が逆転し、モデルは間接的に View クラスを参照するようになりました。これにより、ビューへの変更は IView の実装を脅かさなければモデルに影響を与えませんし、モデルもビューの変更を気にすることなく実装を変更することができます。

これが大まかに DI を行うメリットとその手法なのですが、実際には「誰が View を生成し、Model に渡すのか?」という部分が欠落しています。それを達成するために、たとえば MVP や MVC、MVVM など様々な手法が開発されています。それを手助けするのが DI の仕組みです。

なぜ Unity で DI したいのか

Unity で開発を行う場合、一番重要なパーツはシーンに配置されるゲームオブジェクトです。ゲームオブジェクトがなければ何も画面上に表示されません(DOTS を除く)。ですので、各スクリプトはゲームオブジェクトが存在する前提で動作しますし、また必要になるパーツも大体はゲームオブジェクトが付随しています。

しかし、ゲームオブジェクト自体が管理できないもの(するのが不便なもの)もあります。例えば主人公の体力などはゲームオブジェクトで保持してもいいかもしれませんが、ステージの数や出てくる敵、装備画面の説明文など、ゲームオブジェクトに付随させると大変になるデータも存在します。そのようなデータを MonoBehaviour に所持させ、DontDestroyOnLoad などを使って使い回したり、static な変数でどこかに保持したりしていると、不便な場面がやってきます。

例えば、アクションゲームを作っていて、主人公キャラを作っているとしましょう。Input を使って移動させたり、弾を発射させたり。いざ実装が終わって実行してみると、なぜか NullReferenceException だけがコンソールに表示され何も動きません。なぜか? static で保持しているプレイヤーの初期 HP が初期化されていませんでした。仕方ないので、Start メソッド内に #if UNITY_EDITOR で括った範囲を作り、そこに static 変数を初期化するメソッドを書きます。また実行してみますが、今度は Hp が -1 になっていて、すぐにプレイヤーが死んでしまいました。どうやら、DontDestroyOnLoad な他のスクリプトで初期化が走っていて、-1 に設定してしまっているようです。更に #if UNITY_EDITOR のブロックの中に StaticClass.IsInit = true; と記述し、再度初期化が行われないようにしたのです。さて、これ動いたプログラムが実際のアプリケーションでちゃんと動作すると自信を持って言えるでしょうか? 私はこのような状況に陥ったとき、デプロイされたコードに対して非常に不安になります。

このような状況をなるべく緩和してくれるのが、Zenject の仕組みになるわけです。

Let's Zenject

Zenject 自体の使い方については、他にいろいろ具体的で詳細な使用方法が記載されている記事にお任せして、こちらでは主に設計と、それを考えていく道筋を紹介したいと思います。

ただ、何も知らない状況の方が見ている可能性もありますので、ひとまず読み進められるよう簡単に使い方を説明しておきます。

Zenject では、以下のような流れでインジェクションを行っていきます。

  1. シーンに、SceneContext というコンポーネントのついたゲームオブジェクトを設置します(これはコンテキストメニューから行います)
  2. SceneContext に対して、MonoInstaller というクラスを継承したゲームオブジェクトを指定します。
  3. 指定された MonoInstaller は、DiContainer というクラスのインスタンスにバインド情報を登録します。
  4. シーンがロードされる際、シーン内に存在するスクリプトの [Inject] 属性がついているメンバ全てにインジェクションを行います。

ざっくりとこんな感じの流れになります。

また、Zenject を使う際、SceneContext について以下の前提条件を知っておく必要があります。

  1. シーンごとに1つ設置できる。
  2. 親子関係が作れる
  3. 子のシーン(Additive なシーン)にのみインジェクトすることができる

SceneContext のこの仕様を利用して、インジェクションを行っていきます。

実践

以下のようなコードを、Zenject を使って開発しやすいコードにしていきましょう。

public class WeaponList : MonoBehaviour
{
    [SerializeField] private ListDisplay list;

    private async void Start()
    {
        IEnumerable<Weapons> weapons = await ApiManager.GetWeaponList();
        foreach(var weapon in weapons)
        {
            list.AddItem(new ListDisplayItem { Text = weapon.Name });
        }
    }
}

このコードには、ListDisplay というリストを UI に表示するクラスと、武器データのモデルである Weapons クラス、API を管理し呼び出しを行う ApiManager リストがあります。これらを使用し、武器の一覧を表示する UI だとします。(API を使用するで、きっと所持アイテムリストとかでしょう)

このコードの問題点は、ApiManager.GetWeaponList が確実に動作していることが前提になっているという点です。これは個人開発であれば、特に問題にはならないかもしれませんが、チーム開発の場合問題になる可能性があります。

このコードは、以下のような条件でのみ成立するコードになっています。
1. API の開発が終了している
2. API を取得するコードの開発が終了している
3. API の仕様が変わっておらず、Name を取得できることが確定している

2 と 3 に関しては、ギリギリ自分でなんとかできます。API は自分で実装すればいいですし、仕様が確定せずとも、仮のデータを Name に入れるような実装にしてしまえば一応動きます。ただ 1 はどうしようもありません。サーバが C# で実装されているならともかく、知らない言語で書かれている場合はかなり大変ですし、また実際に開発を行っているメンバーと軋轢を招く可能性もあります。このような状況を避けるために、ApiManager は自分でコントロールできる範囲内に収めるほうが利口だと思われます。

まず、ApiManager はインターフェイス化してしまいましょう。

public interface IWeaponApi
{
    UniTask<IEnumerable<Weapons>> GetWeaponList(); 
}

public class WeaponList : MonoBehaviour
{
    [SerializeField] private ListDisplay list;

    private IWeaponApi weaponApi;

    private async void Start()
    {
        IEnumerable<Weapons> weapons = await weaponApi.GetWeaponList();
        foreach(var weapon in weapons)
        {
            list.AddItem(new ListDisplayItem { Text = weapon.Name });
        }
    }
}

この状態だと、もちろん Start でぬるぽになってしまうので、Zenject を使いインジェクションを行います。

IWeaponApi には、[Inject] 属性をつけてしまいましょう。

    [Inject] private IWeaponApi weaponApi;

次に、MonoInstaller を実装します。(ついでに ApiManager もインスタンス化するように変更しました)

public class ApiInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<ApiManager>().To<IWeaponApi>().AsSingle();
    }
}

これで、WeaponList クラスは ApiManager を参照するのではなく IWeaponApi を参照するようになりました。ここまで来れば、以下のようなクラスを作成し

public class DebugApiInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<DebugWeaponApi>().To<IWeaponApi>().AsSingle();
    }

    private class DebugWeaponApi : IWeaponApi
    {
        public async UniTask<IEnumerable<Weapons>> GetWeaponList()
        {
            return new [] 
            {
                new Weapon { Name = "マスターソード" }
            }  
        }
    }
}

ApiInstaller の代わりに SceneContext に設定してあげればデバッグ用のデータになりました。

運用ポイント① シーンツリーは簡単に開けるようにしよう

さて、ここまでの Zenject を使用したデータの切り替えは、多分 Zenject の入門的な記事を読めばどこにでも書いてあるのですが、これを実運用しようとすると結構面倒くさかったりします。

このコンテキストの切り替え、どう行うのがいいのでしょうか? 毎回設定を変えるごとにシーンを読み直しますか? 依存が多くなった場合、どうしましょう? シーンを正しい順番にロードするために手動で毎回フォルダビューからシーンをロードしているとめんどくさくなってきますよね。

現在私の携わっているプロジェクトには、シーンをロードするための拡張を行いました。コード自体は公開できませんが、イメージ図でご紹介いたします。

まず、ツールバーのような EditorWindow を作りましょう。そこには、以下のようなコードを書きます(C# 風ニセコードです)

interface IExtension
    string Name { get }
    Action Action { get }

IEnumerable<IExtension> extensions = Assembly.LoadAllClassWithInterface<IExtension>();

foreach(var extension in extensions)
    if (GUI.Button(extension.Name))
        extension.Action()

class DebugWeaponSceneExtension : IExtension
    public string Name => "DebugWeaponScene"
    public Action Action => () =>
        SceneManager.NewScene()
        var gameObject = new GameObject()
        var sceneContext = gameObject.AddComponent<SceneContext>()       
        var installer = gameObject.AddComponent<DebugApiInstaller>()
        sceneContext.AddInstaller(installer)

        SceneManager.LoadScene("WeaponScene", Additive)

class ProductWeaponSceneExtension : IExtension
    public string Name => "DebugWeaponScene"
    public Action Action => () =>
        SceneManager.NewScene()
        var gameObject = new GameObject()
        var sceneContext = gameObject.AddComponent<SceneContext>()       
        var installer = gameObject.AddComponent<ApiInstaller>()
        sceneContext.AddInstaller(installer)

        SceneManager.LoadScene("WeaponScene", Additive)

こんな感じで、エディタのツールバーにあるボタンを押すと、必要な組み合わせのシーンができあがるようなものを作りました。

運用ポイント② 「マネジャークラス」は作ってもいい

最初は、何でもかんでもインターフェイスを噛ませていましたが、ある程度動作が安定している場合や(マスターのデータなど)、そもそも外部に影響されないようなものは、マネジャークラスをインジェクトしてもいいと思います。

例えば、シーン遷移を司るクラスは便利です。以下のようなシーンを作ります。

Scene
  -> DebugApiInstallerScene
  -> SceneManagerScene
  -> WeaponListScene

WeaponListScene は 上2つの SceneContext に依存します。あくまでインジェクションを使っているので、SceneManager は必要なくなったら破棄できます。

他にも背景を切り替える BackgroundManager なんかもよく作ってます。

最後に

DI や Zenject の説明にほとんど使い、運用の部分が結構短くなってしまいましたが、実は「シーンの組み合わせを簡単に開けるようにしよう」と「マネジャーはシングルトンじゃなきゃ作ってもいいんだよ」だけで済む話だったりします。個人的にこの方法を使ってからだいぶ安心してコードをデプロイできるようになりました。もちろん実装漏れは出たりするんですが、実装した部分に関しては自信をもてます。みんなで安定したコードを書きましょう!

参考

依存性逆転の原則 - Wikipedia

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

Unityの多言語対応ツールLocalization Toolsをチョット触ってみた

はじめに

京都某所のもくもく会に参加していたら、Unityの中の人からUnityの公式Localization Toolsのことを教えていただきました。未だプレビューなので、情報はこちらのフォーラムからです。

今回はver0.5.1を試しています。インストールするには、Packagesフォルダのmanifest.jsonに"com.unity.localization": "0.5.1-preview"を追加してください。

公式ドキュメント

ver0.5用の公式ドキュメントがこちらにあります。

ほぼここの通りにやれば良いのですが、一部動かなかったりするので、一応やり方を下に書いておきます。

セットアップ手順

まず、Project Settings のLocalizationへ。そこでLocalization SettingsをCreateして、Active Settingsとして設定する。

image.png

すると、Localeセットアップ用の画面になるので、Locale Generatorボタンを押して、言語を選ぶ。

image.png

image.png

選んだらGenerate Localesを押す。

すると、以下のような設定ファイルが作られる。

image.png

言語を切り替える

ドロップダウンメニューによって言語を切り替えます。以下のスクリプトを作ってください。

元情報はこちらですが、少し修整してます。 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Localization.Settings;
using UnityEngine.UI;

public class LocaleDropdown : MonoBehaviour
{
    Dropdown dropdown;

    IEnumerator Start()
    {
        dropdown = GetComponent<Dropdown>();
        // Wait for the localization system to initialize, loading Locales, preloading etc.
        yield return LocalizationSettings.InitializationOperation;

        // Generate list of available Locales
        var options = new List<Dropdown.OptionData>();
        int selected = 0;
        for (int i = 0; i < LocalizationSettings.AvailableLocales.Locales.Count; ++i)
        {
            var locale = LocalizationSettings.AvailableLocales.Locales[i];
            if (LocalizationSettings.SelectedLocale == locale)
                selected = i;
            options.Add(new Dropdown.OptionData(locale.name));
        }
        dropdown.options = options;

        dropdown.value = selected;
        dropdown.onValueChanged.AddListener(LocaleSelected);
    }

    static void LocaleSelected(int index)
    {
        LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.Locales[index];
    }
}

これを作ったら、シーン内にDropdownオブジェクトを作って、それにスクリプトをアタッチしてください。シーンを実行すると、Dropdownボタンで上で設定した言語を選べるようになります。
image.png

テクスチャを言語によって変える

ここで、公式ドキュメントにある言語によるテクスチャ(ここでは国旗)を変更するテストを行います。まず、各言語ごとにテクスチャを割り当てる必要があります。そのために、Windows->Asset Management->Localization Tablesを選んでLocalization Tableを作ります。

image.png

Asset Tablesというウィンドウが開くので、"New Table"を選び、Table Nameを付けてください。

image.png

名前(今回はTextureTable)を付けたら、Createボタンを押してテーブルを作ってください。すると、下のようなテーブルが出来るので、Add New Entryを押して、Key(例えばFlag)、各言語に対応するテクスチャを割り当てます。

image.png

例えばこういう感じです。
image.png

このテクスチャをRawImageに表示するので、シーンにRawImageを追加してください。それに、以下のスクリプトをアタッチします。

スクリプトは、こちらの公式チュートリアルのものを改変しました。

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using System;
using static UnityEngine.Localization.Components.LocalizedAssetBehaviour<UnityEngine.Texture2D>;

public class TextureLocalizationSample : MonoBehaviour
{
    [SerializeField] public Texture2DAssetReference assetRef;
    RawImage rawImage;

    void Start()
    {
        rawImage = GetComponent<RawImage>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.L))
        {
            StartCoroutine("Load");
        }
    }

    public IEnumerator Load()
    {
        var loadOperation = assetRef.LoadAssetAsync();

        yield return loadOperation;

        if (loadOperation.IsDone)
        {
            Debug.Log("Loaded Texture: " + loadOperation.Result.name);
            rawImage.texture = loadOperation.Result;
        }
    }
}

[Serializable]
public class Texture2DAssetReference : LocalizedAssetReference { };

スクリプトをアタッチしたら、AssetRefに割り当てるAssetTable内のKeyをドロップダウンから選べるようになります。
image.png

一旦選ぶと、こちらでも各言語向けのテクスチャを設定することができます。

image.png

準備ができたら、シーンを実行しましょう。ドロップダウンメニューで言語を選んでキーボードの"L"を押すと対応する旗が出るはずです。

Localization.gif

テキストをローカライズする

テキストのローカライズも考え方は同じです。テクスチャと同様にLocalization Tableを作ります。

image.png

テキストのローカライズにはLocalize Stringという付属のコンポーネントを使うのが便利です。

image.png

こちらのコンポーネントのString ReferenceでLocalization TableとKeyを選び、Format Argumentsでテキストを表示するゲームオブジェクトを指定します。Update Stringを上図のように設定しておけば、言語を変えたときに勝手にテキストが変わります。

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

【Unity】VR空間にペンタブレットを持ち込んで板タブを液タブみたいに使ってみた

はじめに

こんにちは、@iykuetbooと申します|
去年に引き続き、UT-virtualアドベントカレンダー2019 に13日目の担当として参加しました。

今回は、UnityとOculusRiftSを使って、VR空間でペンタブレットを使ってみようというお話です。

環境

Windows 10
Unity2018.4.19f1
Wacom Intuos Pro Medium
タブレットドライバのバージョン 6.3.37-3

経緯

ペイントソフトでのイラスト製作や3DCGソフトでのモデリングなどによく使われるペンタブレットですが、大きく2種類に分かれています。

板タブレット(板タブ)
板状無地のタブレット上にペンを走らせることでカーソルを動かせる。ディスプレイを見ながら操作する。
液晶タブレット(液タブ)
タブレット自体が液晶画面になっていて、手元を見ながらペンで直接書くように操作できる。板タブより高い。

で、板タブの方を持っていて使っているのですが、画面を見ながら手元のペンで字や絵を描くのって結構難しいんですよ。
液タブ使いて~、でも高いよね~、なんて友人と話した後、ふと思いつきました。VR機器を使えば板タブでも手元に絵を表示しながら描けるのでは??

実装

というわけで、板タブでも手元を見ながら絵を描ける機能を目指して、Unityを起動しました。

アセットの導入

実現にあたり、なくてはならない3つの機能「ペンタブとUnityの連携」「テクスチャペイント」「VR対応」それぞれに対し、超絶便利なアセットが存在しました。先人達に感謝。

uWintab

凹みTips様の、Unity でペンタブの入力を受け取れるアセットを作ってみた を見ながらunitypackageをインストールします。
Windowsのタブレットドライバのapiから情報を引っ張ってきてくれるアセットです。

インストールしてサンプルシーン開いただけで簡単にUnityペンタブ連携ができちゃいました。

InkPainter

@Es_Program 様の Unityでリアルタイムテクスチャペイント を使用しました。

PaintTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Es.InkPainter; //追加する

public class PaintTest : MonoBehaviour
{
    public InkCanvas canvas;
    public Brush brush;
    public Vector3 paintPosition;

    void Start()
    {
        canvas.Paint(brush,paintPosition));
    }
}

このようにPaintメソッドを呼び出すことでテクスチャにペイントすることができます。詳細はリンク先へ。
nomalmapやheightmapをペイントできたり、インクが垂れる表現ができたりと、ただの平面お絵かきに使うのはもったいなく感じてしまう高機能アセットでした。

OculusIntegration

最後はVR用のアセット。今回はRiftSを使うので、Oculus公式のOculus Integrationをインストールします。
Oculus/VR/Prefabs内にあるOVRCameraRig.prefabをシーンに置くだけで使えます。
機器に依存する処理はしていないので、QuestやRiftを使ったり、SteamVRでVIVEを使ったりしても大丈夫です。

ペンタブで入力した位置にペイントする

InkPainterのPaintメソッドは、ペイントしたい位置へのRaycastHitまたはその位置のWorld座標を引数に必要とします。
一方、uWintabで取得できるのはタブレット内のペン位置(xとy,ともに0-1)です。

このままではペンの入力位置にペイントすることが出来ないので、取得したペン位置をWorld座標に変換することにします。
uWintabのTabletクラスを継承し、ワールド座標を取得できるようにした新しいクラスを生成してそれに置き換えます。

TabletExtend.cs
using uWintab;

public class TabletExtend : Tablet
{
    public Transform board;

    public Vector3 GetLocalTouchPosion
    {
        get
        {
            float x_, y_, z_;
            x_ = (x - 0.5f) * board.localScale.x;
            y_ = (1f - 0.5f) * board.localScale.y;
            z_ = (y - 0.5f) * board.localScale.z;

            return board.localPosition + new Vector3(x_, y_, z_);
        }
    }

    public Vector3 GetWorldTouchPosion
    {
        get
        {
            return transform.TransformPoint(GetLocalTouchPosion);
        }
    }
}

とりあえずuWintabのサンプルシーンからペンやタブレットのオブジェクトをコピーしてきて、TabletにアタッチされているTablet.csを上記に置き換えます。そして、子のBoardオブジェクトにInkPainterのInkCanvas.csをアタッチしました。

これでWorld座標の取得が可能になったので、実際にペイントするためのスクリプトを用意します。

PaintOnBoard.cs
public class PaintOnBoard : MonoBehaviour
{
    [SerializeField] InkCanvas canvas;
    [SerializeField] TabletExtend tablet;
    public Brush brush;

    // Update is called once per frame
    void Update()
    {
        if (tablet.pressure > 0.1f)
        {
            canvas.Paint(brush, tablet.GetWorldTouchPosion);
        }
    }
}

これを適当なゲームオブジェクトにアタッチし、参照先をインスペクタから設定すれば、、、
ペンタブの入力位置にペイントできるようになりました!!(VRじゃなくてもこれだけで楽しい)

VRでやってみる

先ほどのシーンにOVRCameraRigを配置し、位置や大きさを調整してヘッドセットで覗いてみます。

!!

思った通りの線が描ける!!!
手元を見ながら描ける!!!
細かい文字も綺麗に書ける!!!

さらに色々

せっかくVRなんだ、タブレットよりもデカいキャンバスに描きたい!
というわけで、Inkcanvas.csを別のプレーンにアタッチして、取得できるWorld座標と合わせることでタブレットより大きいキャンバスに書き込めるようしたり。

せっかくペンの筆圧取得できるんだ、筆圧に応じて太さ変えたい!
というわけで、Tabletからpressure取得してその値をbrushのScaleに反映させたり。

せっかくブラシの設定できるんだ、ブラシの色とか大きさとかテクスチャ変えたい!
というわけで、TabletのexpKeyでブラシの色や大きさを変えられるようにしたり。

ペンを速く動かした時に点線になってしまうのは嫌だ!
というわけで、Paintメソッドの呼び出し回数を増やしてみたり。

色々やった結果こんな感じになりました!
ezgif.com-video-to-gif.gif
(実装方法詳細書いてるとアドベントカレンダー間に合わないから雑でも許して)

コード詳細
BrushSizeChange.cs
using Es.InkPainter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BrushSizeChange : MonoBehaviour
{
    [SerializeField] TabletExtend tablet;

    [SerializeField] PaintOnBoard painter;
    Brush brush;

    [SerializeField] float[] sizes;//変更先候補のサイズの配列
    float brushSize;//筆圧最大の時のBrushのサイズ(基本サイズ)
    int index;

    [SerializeField] Transform colorSampleRect;//前方にBrushの見本をUI表示

    // Start is called before the first frame update
    void Start()
    {
        ChangeTo(sizes[0]);
        brush = painter.brush;
    }

    // Update is called once per frame
    void Update()
    {
        if (tablet.GetExpKeyDown(1))
        {
            index = (index + 1) % sizes.Length;
            ChangeTo(sizes[index]);
        }

        if(tablet.pressure > 0)
        {
            brush.Scale = brushSize * tablet.pressure;//基本サイズに筆圧に応じて倍率をかける
        }

    }

    void ChangeTo(float size)//基本サイズを変更
    {
        colorSampleRect.localScale = size / sizes[sizes.Length-1] * Vector3.one;
        brushSize = size;
    }
}
BrushColorChange.cs
using Es.InkPainter;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BrushColorChange : MonoBehaviour
{
    [SerializeField] TabletExtend tablet;

    [SerializeField] PaintOnBoard painter;
    Brush brush;

    [SerializeField] Color[] colors;//変更先候補の色配列
    int index;

    [SerializeField] Material colorSampleMat;

    // Start is called before the first frame update
    void Start()
    {
        brush = painter.brush;
    }

    // Update is called once per frame
    void Update()
    {
        if (tablet.GetExpKeyDown(0))
        {
            index = (index + 1) % colors.Length;//変更先の色インデックスを変更
            ChangeTo(colors[index]);
        }
    }

    void ChangeTo(Color color)//Brushの色変更
    {
        colorSampleMat.color = color;
        brush.Color = color;
    }
}
PaintOnBoard.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Es.InkPainter;

public class PaintOnBoard : MonoBehaviour
{
    [SerializeField] InkCanvas canvas;
    [SerializeField] TabletExtend tablet;
    public Brush brush;

    Vector3 currentTouchPosition;
    Vector3 preTouchPosition;

    bool drawLine = false;
    [Range(1,10)] public int Density;//点線になるのを防ぐ

    // Update is called once per frame
    void Update()
    {

        if (tablet.pressure > 0.1f)
        {
            currentTouchPosition = tablet.GetWorldTouchPosion;
            canvas.Paint(brush, currentTouchPosition);

            if(drawLine)//前フレームでPaintしていた場合
            {
                if(Density > 1)
                {
                    for(int i = 1; i < Density; i++)
                    {
                        canvas.Paint(brush, Vector3.Lerp(currentTouchPosition, preTouchPosition, 1f/Density * i));//補間して滑らかな直線に
                    }
                }

                preTouchPosition = currentTouchPosition;
            }
            else
            {
                drawLine = true;
                preTouchPosition = currentTouchPosition;
            }
        }
        else
        {
            drawLine = false;//書くのをやめたときにフラグを下す
        }
    }
}

詰まった点

途中困った点とか謎のバグとかあったのでいくつか触れておきます。

・GetExpKeyDownが上手く動かない
uWintabのTablet.csのGetExpKeyDownメソッドが、ExpKeyを押し始めたタイミングのみTrueになってほしいのに、押している間常にTrueとなってしまいました。WintabAPIとの連携のところでの処理回数の違いとかが原因なのかなと素人ながらに想像していますが詳細不明...。

・タブレットの指操作を取得したい
ペンタブレットはペン入力だけでなく、設定によってはトラックパッドと同様に指での操作も可能です。ペンで描きながら指でキャンバスを移動させたいなーと思い指入力の有効化の方法を調べると、Wacom「Intuos Pro」タッチパネルで出来る事とオンオフの方法というサイトが。しかし設定を開いても「タッチ入力を有効化する」というトグルが見当たりませんでした。その後いろいろ調べると、このタブレットは本体のハード側にタッチ入力の切り替えボタンが付いていて解決
タッチ入力をunityで取得しようとすると、どうやら通常のトラックパッドと同様の処理になっているらしく、Input.mousePosition()を使うしかなさそう。マウスとは別に処理したいのですが、その方法は未発見です。

感想

実際にやってみると、ディスプレイを見ながらペンタブを使うのに比べ、相当正確に書けるというのが分かった。液タブみたいな使用感という当初の目標はバッチリ達成

今回じつは、VR空間と現実のペンタブの位置はトラッキングしているわけではなく、シーン上で位置を合わせているだけでした。それでも、手元、特にペン先が見えてると現実で描くのとほぼ同じ感覚で細かい字まで思い通りに書けました視覚をダマすのはやっぱり効きますね。もっといろんなブラシとか搭載したり、ペイントソフトと連携したりして本格的に描けるようにしたら絶対楽しい。

イラスト方面なら、参考資料の絵とか3Dオブジェクトとか前方に浮かべて見ながら描いたり、VR書斎つくってHMD被るだけで散らかった部屋でも一瞬で創作環境に入れたり...
他にも、文字とかも普通に書けるので色々整えればVRでの会議とかにバリ使えそう。文字認識とかと組み合わせてもよさげ。
などなど、色々妄想が膨らんで楽しかったです。

余談

冒頭で、液晶タブレット高いという話を出しましたが、VR機材もそれなりに高い。どうなんだろうなと思い値段比較してみました。
さっと調べただけなので参考程度に...(2019.12現在)

液タブ
安:XP-PEN Artist12 ¥21,998
中:Wacom Cintiq 16 (DTK1660K0D) ¥74,580
高:Wacom Cintiq Pro 32 (DTH-3220/K0) ¥404,800
VRヘッドセット
安:Oculus Go 32GB ¥17,500
中:Oculus Quest 64GB ¥49,800
高:VIVE Pro Full Kit ¥147,880

やっぱりピンキリで求める性能によって全然違ってきそうですが、VR機材の方が高そうなイメージだったのにそうでもなかった...。

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

UnityでVSCodeの補完機能が使えない

PC:Win10
Unityのバージョンを2019にするとVSCodeをUnityで使いやすくなると聞いたので開いたが、もろもろのソフトを入れたはずなのに補完機能が使えなかったので共有。実は今まで補完機能をほとんど使っていなかった、、、

[参考にしたサイト]
・unityでVSCodeを開く適切な方法として
https://qiita.com/honmaaax/items/da39d7b854c39c3ea110

・入れたソフトな、設定など
https://qiita.com/riekure/items/c45868f37a187f8e1d69
https://qiita.com/kamanii24/items/399f956f2bf6bec76884

これらの記事を参考にさせてもらって入れたんだけど補完ができなかった。

VSCodeに以下の文言が出ていた
OmniSharp.MSBuild.ProjectManager Attempted to update project that is not loaded:

検索したところこのサイトに引っ掛かり
https://stackoverflow.com/questions/55535177/omnisharp-msbuild-projectmanager-attempted-to-update-project-that-is-not-loaded

image.png
この回答のようにVSCodeの左上にあるファイルから基本設定→設定をクリック
すると、検索できる窓があるので
image.png
ここで編集をクリック
コードをコピペ
image.png

C#のスクリプト編集に戻ってv3と打つとVector3と出てきた。
これでおそらく動いたと思われる。

もし参考になれば!

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

VFX GraphにおけるVFX Property Binderの活用

この記事はUnity Advent Calender 2019 18日目の記事です。

Unityの新しいエフェクト制作ツールであるVFX Graph、皆さんは活用されていますでしょうか?
本記事ではVFX Graphを使う上で知っておくと便利な機能、VFX Property Binderの紹介をします。
*Unityのバージョンについては2019.3.0b12を使用しています。バージョンによって単語や操作について相違点がある場合もあるのでご注意ください。

VFX Graph

Unityの新しいエフェクトツールです。Compute ShaderによるGPUパーティクルとノードベースのエディターが特徴で、Package Managerから導入することができます。
映像作成やxRコンテンツなど、様々な用途にUnityが進化を遂げていく中で自分がかなり期待している機能の一つです。
機能自体の導入や基本操作に関しては省略しますので、そこから知っていきたい方は@tan-yさんの記事がかなり丁寧なので参考にしてみてください。

Visual Effect Graph 入門 @tan-y
https://qiita.com/tan-y/items/cd6fc58674d6f0c54d0b

VFX Property Binder

VFX Property Binder(以下 : Property Binder)は、シーン内の情報をVFX Graphに伝達して演出のパラメータとして使うための機能です。
出た当初はParameter Binderという名前だった気がするのですが、いつの間にかProperty Binderという名前に変わっていました。
特定のオブジェクトのTransform、Colliderなど様々なパラメータに基づいた演出を可能にしてくれます。スクリプトもほぼ書かずに実現できるためかなりお手軽な機能です。
この機能を使うことで、オブジェクトの座標に従ってパーティクルを移動させたり、特定のオブジェクトとの衝突でパーティクルの挙動を変えたりと、演出にシーンに基づいた動的な変化を与えることが可能です。

スクリーンショット 2019-12-14 17.06.55.png

Property Binderはコンポーネントとして提供されており、インスペクター上で様々な設定を行うことができます。
VFX Graphと連動させたい要素ごとに○○ Binderというコンポーネントが用意されており、上の画像のようにProperty Binderのコンポーネントの設定にセットしていくことになります。
Property Binderが親コンポーネントで、そこに子となる要素ごとのBinderのコンポーネントを設定していく感じです。

追加したコンポーネントは画像のようにインスペクターのメニューから設定可能で、メニューから用途に合わせて選択します。
色々な用途のその中のコンポーネントの中からいくつかの種類を紹介します。

使い方

まずは適当なオブジェクトにProperty Binderをアタッチします。
スクリーンショット 2019-12-14 17.21.44.png
アタッチすると自動的にVisual Effectのコンポーネントもアタッチされます。
Visual EffectはVFX Graphのエフェクトをシーン内で使うためのコンポーネントで、Property Binderが作用させる対象になります。
Property Binderは内部でGetComponentしてVisual Effectを参照しているため、同じオブジェクトにアタッチすることが必要になります。

Property Bindingsのプラスボタンを押すと前述のパラメータごとのコンポーネントを設定することができます。
Position Binderを例として追加して説明していきます。

スクリーンショット 2019-12-14 18.15.56.png

追加すると、対応するコンポーネントのアタッチとプロパティ名の設定をする必要があります。
コンポーネントのアタッチについては任意のシーン内のコンポーネントを設定してください。Position BinderならVFX Graphに伝達させたいオブジェクトのTransformを設定することになります。
プロパティ名はVFX Graphに値を伝達するためのVFX Graph側のプロパティ名になります。

スクリーンショット 2019-12-14 18.11.11.png

Positionというプロパティ名を指定する場合は上の画像のようにVFX Graph側でプロパティを作成する必要があります。
外部から伝達させるためにプロパティのExposedのオプションはオンにしないといけないので注意しましょう。

スクリーンショット 2019-12-14 18.20.22.png

設定が完了すると、緑のマークになります。ここが緑にならない場合、設定に問題がある場合があるので見直してみてください。(大抵はVFX Graph側の設定です。)

スクリーンショット 2019-12-14 18.21.36.png

あとはVFX Graph側でノードに繋げれば、シーン内のパラメータを使うことができます。

Property Binderの種類

どういったパラメータを伝達させることができるのか、いくつかProperty Binderの種類について説明します。

Position Binder

オブジェクトのPositionの情報をVFX Graphに伝達するためのコンポーネントです。
キャラクターの位置で演出を変えたり、演出の注視点を変えたり、位置に関する動的な変更を加えるのに便利です。

Rigid Body Collision Event Binder

Collisionにおける衝突イベントをVFX Graphに伝達するためのコンポーネントです。
VFX GraphはSpawnノードをうまく活用することで、一つのGraph内で複数のエフェクトを定義し、それらの生成タイミングを切り替えることが可能です。
その際に、生成タイミングの切り替えでこのコンポーネントを使うことで、衝突したら爆発のエフェクトに切り替えたい!といったゲームの演出でありがちな要件をスクリプトを書かずに実現することができます。

Light Binder

Lightの色や輝度をVFX Graphに伝達するためのコンポーネントです。
シーン内のLightに合わせた演出をしたい!という場合にいい感じにこなしてくれるのがこのコンポーネントです。
対象のLightを設定するだけで色、輝度、効果範囲のパラメータをエフェクトと連動させることができます。

Mouse Event Binder

マウスのイベントをVFX Graphに伝達するためのコンポーネントです。
これまで紹介したコンポーネントはシーンに合わせて動的に演出を変更するためのものでしたが、Mouese Event Binderはユーザー入力によってエフェクトを制御するために使用します。

UI Dropdown Binder

uGUIのDropdownで選択した情報をVFX Graphに伝達するためのコンポーネントです。
uGUIでエフェクトのプレビュー画面が作りたい、といった時にちょっとしたスクリプトを書いて作ることはあると思うのですが、それをコンポーネントで完結させることが可能な機能です。

ここで紹介したものだけでも、様々な用途に合わせてコンポーネントが用意されているのが分かるかと思います。
個人的にはPosition Binderだけでもかなり演出の手助けになってます。

使ってみる

試しに簡単な例でPosition Binderを使ってみます。
まずは適当なVFX Graphのエフェクトを作成し、次のようなGraphの設定だけ追加します。

スクリーンショット 2019-12-17 20.55.14.png

ざっくり説明すると、これは特定の座標に向かって適当に加速をするような設定になっています。
まず、
パラメータとして追加したPositionノードとパーティクルの現在位置であるTransform (Position) ノードと共に差分を算出するSubstractノードに繋ぎ、現在位置からPositionまでの距離を出力します。
この値に適当な係数をMultiplyで掛けて、パーティクルの更新を行うUpdateで速度を加算するAdd Velocityブロックに繋いで完成です。

そしてエフェクトをシーン上に配置します。
エフェクト自体はこんな感じでデフォルトのテクスチャがわき出る感じになってるかと思います。

スクリーンショット 2019-12-17 21.02.06.png

エフェクトを配置したらPosition Binderの設定をします。
今回はわかりやすいようにSphereを生成して、その座標をPosition Binderで伝搬させてみます。

スクリーンショット 2019-12-17 21.05.02.png

ヒエラルキーからSphereを生成して、エフェクトのオブジェクトにProperty Binderのアタッチ、及び上の画像のような設定をしたPosition Binderを設定すれば完了です。
これで先程VFX Graphで追加したPositionにSphereの座標が伝搬されます。
後はシーン上でSphereを動かしてみると...

ezgif.com-video-to-gif.gif

Sphereに沿ってエフェクトの動く方向が変わりました!
このようにランタイムだけでなく、シーン上でも確認できるので、気軽に試すことができます。

もっと応用的な例がみたい!という方は公式のリファレンスもそうなのですが、下記のブログを参考にしてみてください。

Unityで主人公が通った場所の草をなぎ倒す
https://gametukurikata.com/effect/bendgrass

上記のブログでは実際に有用な草のフィールド演出を元にしているため、応用して演出に活かすことがやりやすい内容になっています。
VFX Graphのノードの使い方についても参考になるものがあるので、興味がある方は一度読んでみて欲しいです。

まとめ

Parameter Binderを使うことで、VFX Graphの演出の幅をかなり広げることができます。
静的な演出だったものが動的な演出になることで、コンテンツとして伝えられる表現の幅も広くすることができます。
VFX Graph、楽しいので皆さんも触ってみましょう!

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

【Unity】UniversalRPでカスタムポストプロセスを作る【ZoomBlur】

概要

本記事はUnity #2 Advent Calendar 2019 の15日目の記事です。
今回UnityのUniversalRenderPipeline(以下、UniversalRP)を使用してZoomBlur【咆哮のポストエフェクト】を実装しました。
ZoomBlur.gif

この素晴らしいドラゴンはアセットストアから無料でダウンロードしました。感謝:relaxed:

環境

下記の環境で実装しております。

  • Unity2019.3.0b12
  • Windows10
  • UniversalRP 7.1.5

UniversalRPのVolumeとPostProcessingStackv2の比較

ppsv2に相当する機能が、UniversalRPではVolumeという名前に変わっていました。
他にもクラス名を比較すると下記のような対応関係になっています。

ppsv2 UniversalRenderPipeline
PostProcessEffectSettings VolumeComponent
PostProcessProfile VolumeProfile
PostProcessManager VolumeManager

ppsv2ではPostProcessEffectSettingsPostProcessEffectRendererを継承してカスタムエフェクトを作成しました。
参考:Writing-Custom-Effects

UniversalRPでは公式的にカスタムエフェクトの実装方法は提供されていませんが
下記のような手法で実装できました。

実装

まずはVolumeComponentを継承してカスタムエフェクトのパラメータを定義してみましょう。

VolumeComponent

VolumeComponentを継承してParameterを定義します。

ZoomBlur.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ZoomBlur : VolumeComponent, IPostProcessComponent
{
    [Range(0f, 100f), Tooltip("強くすることで強いBlurがかかります。")]
    public FloatParameter focusPower = new FloatParameter(0f);

    [Range(0, 10), Tooltip("値が大きいほど綺麗にでますが負荷が高まるので注意してください。")]
    public IntParameter focusDetail = new IntParameter(5);

    [Tooltip("ブラーの中心座標。スクリーンの中心を(0,0)としています。")]
    public Vector2Parameter focusScreenPosition = new Vector2Parameter(Vector2.zero);

    public bool IsActive() => focusPower.value > 0f;

    public bool IsTileCompatible() => false;
}

ScriptableRendererFeatureとScriptableRenderPass

次に独自の描画パスを差し込めるようにするために2つのクラスとシェーダーを実装します。

ZoomBlurRenderFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class ZoomBlurRenderFeature : ScriptableRendererFeature
{
    ZoomBlurPass zoomBlurPass;

    public override void Create()
    {
        zoomBlurPass = new ZoomBlurPass(RenderPassEvent.BeforeRenderingPostProcessing);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        zoomBlurPass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(zoomBlurPass);
    }
}
ZoomBlurPass.cs
public class ZoomBlurPass : ScriptableRenderPass
{
    static readonly string k_RenderTag = "Render ZoomBlur Effects";
    static readonly int MainTexId = Shader.PropertyToID("_MainTex");
    static readonly int TempTargetId = Shader.PropertyToID("_TempTargetZoomBlur");
    static readonly int FocusPowerId = Shader.PropertyToID("_FocusPower");
    static readonly int FocusDetailId = Shader.PropertyToID("_FocusDetail");
    static readonly int FocusScreenPositionId = Shader.PropertyToID("_FocusScreenPosition");
    ZoomBlur zoomBlur;
    Material zoomBlurMaterial;
    RenderTargetIdentifier currentTarget;

    public ZoomBlurPass(RenderPassEvent evt)
    {
        renderPassEvent = evt;
        var shader = Shader.Find("PostEffect/ZoomBlur");
        if (shader == null)
        {
            Debug.LogError("Shader not found.");
            return;
        }
        zoomBlurMaterial = CoreUtils.CreateEngineMaterial(shader);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (zoomBlurMaterial == null)
        {
            Debug.LogError("Material not created.");
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled) return;

        var stack = VolumeManager.instance.stack;
        zoomBlur = stack.GetComponent<ZoomBlur>();
        if (zoomBlur == null) { return; }
        if (!zoomBlur.IsActive()) { return; }

        var cmd = CommandBufferPool.Get(k_RenderTag);
        Render(cmd, ref renderingData);
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Setup(in RenderTargetIdentifier currentTarget)
    {
        this.currentTarget = currentTarget;
    }

    void Render(CommandBuffer cmd, ref RenderingData renderingData)
    {
        ref var cameraData = ref renderingData.cameraData;
        var source = currentTarget;
        int destination = TempTargetId;

        var w = cameraData.camera.scaledPixelWidth;
        var h = cameraData.camera.scaledPixelHeight;
        zoomBlurMaterial.SetFloat(FocusPowerId, zoomBlur.focusPower.value);
        zoomBlurMaterial.SetInt(FocusDetailId, zoomBlur.focusDetail.value);
        zoomBlurMaterial.SetVector(FocusScreenPositionId, zoomBlur.focusScreenPosition.value);

        int shaderPass = 0;
        cmd.SetGlobalTexture(MainTexId, source);
        cmd.GetTemporaryRT(destination, w, h, 0, FilterMode.Point, RenderTextureFormat.Default);
        cmd.Blit(source, destination);
        cmd.Blit(destination, source, zoomBlurMaterial, shaderPass);
    }
}

シェーダー

シェーダーはShaderToyをUnity用にカスタムして使わせていただきました。

ZoomBlur.shader
Shader "PostEffect/ZoomBlur"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Cull Off ZWrite Off ZTest Always
        Tags { "RenderPipeline" = "UniversalPipeline"}
        Pass
        {
            CGPROGRAM
                #pragma vertex Vert
                #pragma fragment Frag

                sampler2D _MainTex;
                float2 _FocusScreenPosition;
                float _FocusPower;
                 int _FocusDetail;

                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;
                }

                float4 Frag (v2f i) : SV_Target
                {
                    float2 screenPoint = _FocusScreenPosition + _ScreenParams.xy/2;
                    float2 uv = i.uv;
                    float2 mousePos = (screenPoint.xy / _ScreenParams.xy);
                    float2 focus = uv - mousePos;
                    float4 outColor = float4(0, 0, 0, 1);
                    for (int i=0; i<_FocusDetail; i++) {
                        float power = 1.0 - _FocusPower * (1.0/_ScreenParams.x) * float(i);
                        outColor.rgb += tex2D(_MainTex , focus * power + mousePos).rgb;
                    }
                    outColor.rgb *= 1.0 / float(_FocusDetail);
                    return outColor;
                }
            ENDCG
        }
    }
}

セットアップ

Main CameraのPost Processingはチェックを忘れずに入れておきます。

2019-12-08_23h36_51.png

Volumeの追加

ヒエラルキーの右クリック/Volume/Global Volumeでオブジェクトを追加します。
2019-12-09_11h30_53.png

Add OverrideにてZoom Blurを追加します。
プロファイルが設定されていなければNewボタンでプロファイルアセットを作成します。
2019-12-08_23h32_33.png
(Bloomは趣味でつけてます)

RenderPipelineアセットの作成とRenderFeatureの登録

UniversalRenderPipelineAssetの作成をします。
Create/Rendering/Universal Render Pipeline/Pipeline Asset(Forward Renderer)
2019-12-09_11h33_41.png

同時に作成されるUniversalRenderPipelineAsset_Renderer+ボタンでZoom Blur Render Featureを登録します。
2019-12-09_11h36_24.png

ProjectSettings/Graphics/Scriptable Render Pipeline SettingsUniversalRenderPipelineAssetをセットします。
2019-12-09_11h41_03.png

これで独自のポストプロセス描画パスが反映されるようになりました。

アニメーター連携

アニメーターから連携してZoomBlurのパラメータを操作できるように
コントローラとなるコンポーネントを作成します。

ZoomBlurController.cs
using UnityEngine;
using UnityEngine.Rendering;

[ExecuteAlways]
public class ZoomBlurController : MonoBehaviour
{
    public VolumeProfile volumeProfile; // プロジェクトに作ったPostProcessVolume Profileをアタッチします
    [Range(0f, 100f)]
    public float focusPower = 10f;
    [Range(0, 10)]
    public int focusDetail = 5;
    public Vector2 focusScreenPosition = Vector2.zero;
    ZoomBlur zoomBlur;

    void Update()
    {
        if (volumeProfile == null) return;
        if (zoomBlur == null) volumeProfile.TryGet<ZoomBlur>(out zoomBlur);
        if (zoomBlur == null) return;

        zoomBlur.focusPower.value = focusPower;
        zoomBlur.focusDetail.value = focusDetail;
        zoomBlur.focusScreenPosition.value = focusScreenPosition;
    }
}

2019-12-14_17h23_50.png

プロジェクトデータ

サンプルプロジェクトデータをGitHubにアップしましたので
ご自由にお使いください。

参考サイト様

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

【エディタ拡張】UIElementsとTreeViewでJsonツリービューを作る

はじめに

本記事はgumi Inc. Advent Calendar 2019の15日目の記事です。
Unityのエディタ拡張のUIElementsとIMGUIのコントロールであるTreeViewでJsonビューワを作ってみました。
2019-12-14_02h30_16.png

使い方

  • 左側にJsonテキストをコピペし、>ボタンを押すと右側にツリー構造で表示します。

JSON Editor Onlineを参考にしました。

環境

  • Unity2019.2.5f1
  • Windows10

必要アセット

今回、JsonパーサーにMiniJsonを使用しております。
こちらよりコードをダウンロードしてください。

コード

JsonEditor.cs
using System.Linq;
using MiniJSON;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class JsonEditor : EditorWindow
{
    public class JsonNode
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
        public JsonNode Parent { get; private set; }
        List<JsonNode> children = new List<JsonNode>();
        public List<JsonNode> Children => children;

        public IEnumerable<int> Ids
        {
            get
            {
                yield return Id;
                foreach (var child in children)
                {
                    foreach (var childId in child.Ids)
                    {
                        yield return childId;
                    }
                }
            }
        }

        public void AddChild(JsonNode child)
        {
            if (child.Parent != null)
            {
                child.Parent.RemoveChild(child);
            }
            Children.Add(child);
            child.Parent = this;
        }

        public void RemoveChild(JsonNode child)
        {
            if (Children.Contains(child))
            {
                Children.Remove(child);
                child.Parent = null;
            }
        }
    }

    public class JsonTreeView : TreeView
    {
        class JsonTreeViewItem : TreeViewItem
        {
            public JsonNode Data { get; set; }
        }

        JsonNode[] baseElements;

        public JsonTreeView(TreeViewState treeViewState, MultiColumnHeader multiColumnHeader) : base(treeViewState, multiColumnHeader)
        {
        }

        public void Setup(JsonNode[] baseElements)
        {
            this.baseElements = baseElements;
            Reload();
        }

        protected override TreeViewItem BuildRoot()
        {
            return new TreeViewItem { id = 0, depth = -1, displayName = "Root" };
        }

        protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
        {
            var rows = GetRows() ?? new List<TreeViewItem>();
            rows.Clear();

            foreach (var baseElement in baseElements)
            {
                var baseItem = CreateTreeViewItem(baseElement);
                root.AddChild(baseItem);
                rows.Add(baseItem);
                if (baseElement.Children.Count >= 1)
                {
                    if (IsExpanded(baseItem.id))
                    {
                        AddChildrenRecursive(baseElement, baseItem, rows);
                    }
                    else
                    {
                        baseItem.children = CreateChildListForCollapsedParent();
                    }
                }
            }
            SetupDepthsFromParentsAndChildren(root);

            return rows;
        }

        void AddChildrenRecursive(JsonNode element, TreeViewItem item, IList<TreeViewItem> rows)
        {
            foreach (var childElement in element.Children)
            {
                var childItem = CreateTreeViewItem(childElement);
                item.AddChild(childItem);
                rows.Add(childItem);
                if (childElement.Children.Count >= 1)
                {
                    if (IsExpanded(childElement.Id))
                    {
                        AddChildrenRecursive(childElement, childItem, rows);
                    }
                    else
                    {
                        childItem.children = CreateChildListForCollapsedParent();
                    }
                }
            }
        }

        JsonTreeViewItem CreateTreeViewItem(JsonNode model)
        {
            return new JsonTreeViewItem { id = model.Id, displayName = model.Key, Data = model };
        }

        protected override void RowGUI(RowGUIArgs args)
        {
            var item = (JsonTreeViewItem)args.item;
            for (int i = 0; i < args.GetNumVisibleColumns(); ++i)
            {
                var cellRect = args.GetCellRect(i);
                var columnIndex = args.GetColumn(i);
                if (columnIndex == 0)
                {
                    base.RowGUI(args);
                }
                else if (columnIndex == 1)
                {
                    GUI.TextField(cellRect, item.Data.Value);
                }
            }
        }
    }

    JsonTreeView treeView;
    TextField inputText;
    int currentId;
    List<JsonNode> roots = new List<JsonNode>();

    [MenuItem("Window/JsonEditor")]
    static void Open()
    {
        GetWindow<JsonEditor>();
    }

    void OnEnable()
    {
        currentId = 0;

        BuildToolbarUI();
        BuildTreeView();
        Refresh();

        var scroller = new ScrollView();
        rootVisualElement.Add(scroller);

        var container = new VisualElement();
        container.style.flexDirection = FlexDirection.Row;
        scroller.Add(container);

        inputText = new TextField();
        inputText.style.width = 200;
        inputText.style.paddingBottom = 10;
        inputText.style.paddingTop = 10;
        inputText.style.paddingLeft = 10;
        inputText.style.paddingRight = 10;
        inputText.style.height = Screen.height - 100;
        inputText.multiline = true;
        foreach (var child in inputText.Children())
        {
            child.style.unityTextAlign = TextAnchor.UpperLeft;
        }
        container.Add(inputText);

        var convertButton = new Button(() => Convert());
        convertButton.text = ">";
        convertButton.style.width = 44;
        convertButton.style.height = 44;

        var space = new ToolbarSpacer();
        space.style.width = 100;
        space.flex = false;
        space.style.backgroundColor = rootVisualElement.style.backgroundColor;
        space.style.paddingBottom = 10;
        space.style.paddingTop = 10;
        space.style.paddingLeft = 10;
        space.style.paddingRight = 10;
        space.Add(convertButton);
        container.Add(space);
        var treeContainer = new IMGUIContainer(() =>
        {
            var rect = EditorGUILayout.GetControlRect(false, Screen.height);
            treeView.OnGUI(rect);
        });
        treeContainer.style.width = Screen.width;
        treeContainer.style.height = Screen.height;
        container.Add(treeContainer);
    }

    void BuildToolbarUI()
    {
        var toolbar = new Toolbar();
        var clearButton = new ToolbarButton(() =>
        {
            roots.Clear();
            Refresh();
        });

        var expandAllButton = new ToolbarButton(() => treeView.SetExpanded(roots[0].Ids.ToArray()));
        expandAllButton.text = "ExpandAll";
        toolbar.Add(expandAllButton);

        var collapseAllButton = new ToolbarButton(() => treeView.CollapseAll());
        collapseAllButton.text = "CollapseAll";
        toolbar.Add(collapseAllButton);

        clearButton.text = "Clear";
        toolbar.Add(clearButton);
        rootVisualElement.Add(toolbar);
    }

    void BuildTreeView()
    {
        var nameColumn = new MultiColumnHeaderState.Column()
        {
            headerContent = new GUIContent("Key"),
            headerTextAlignment = TextAlignment.Center,
            canSort = false,
            width = 200,
            minWidth = 50,
            autoResize = true,
            allowToggleVisibility = false
        };
        var descriptionColumn = new MultiColumnHeaderState.Column()
        {
            headerContent = new GUIContent("Value"),
            headerTextAlignment = TextAlignment.Center,
            canSort = false,
            width = Screen.width / 2 - 100,
            minWidth = 50,
            autoResize = true,
            allowToggleVisibility = false
        };

        var headerState = new MultiColumnHeaderState(new MultiColumnHeaderState.Column[] { nameColumn, descriptionColumn });
        var multiColumnHeader = new MultiColumnHeader(headerState);
        var treeViewState = new TreeViewState();
        this.treeView = new JsonTreeView(treeViewState, multiColumnHeader);
    }

    void Convert()
    {
        roots.Clear();
        currentId = 0;

        var root = AddTree(inputText.value, string.Empty);
        roots.Add(root);

        treeView.SetExpanded(new int[] { root.Id });
        Refresh();
    }

    void Refresh()
    {
        treeView.Setup(roots.ToArray());
    }

    JsonNode AddTree(string json, string rootLabel)
    {
        var root = new JsonNode { Id = currentId++, Key = rootLabel };
        var dict = Json.Deserialize(json) as Dictionary<string, object>;
        AddNode(root, dict);
        return root;
    }

    void AddNode(JsonNode element, object node)
    {
        if (node == null) { return; }
        if (node.GetType() == typeof(Dictionary<string, object>))
        {
            var dict = node as Dictionary<string, object>;
            foreach (var childPair in dict)
            {
                AddNode(element, childPair);
            }
        }
        else if (node.GetType() == typeof(KeyValuePair<string, object>))
        {
            var pair = (KeyValuePair<string, object>)node;
            var child = new JsonNode { Id = currentId++, Key = pair.Key };
            if (pair.Value != null)
            {
                var valueType = pair.Value.GetType();
                if (valueType != typeof(Dictionary<string, object>) &&
                    valueType != typeof(List<object>)
                    )
                {
                    child.Value = pair.Value.ToString();
                }
            }
            element.AddChild(child);

            AddNode(child, pair.Value);
        }
        else if (node.GetType() == typeof(List<object>))
        {
            var list = node as List<object>;
            var index = 0;
            foreach (var item in list)
            {
                if (item != null)
                {
                    var val = item.GetType() == typeof(Dictionary<string, object>) ? string.Empty : item.ToString();
                    var child = new JsonNode { Id = currentId++, Key = (index++).ToString(), Value = val };
                    element.AddChild(child);
                    AddNode(child, item);
                }
                else
                {
                    var child = new JsonNode { Id = currentId++, Key = (index++).ToString(), Value = "null" };
                    element.AddChild(child);
                }
            }
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

バーチャルキャラクターの表情表現のためのデバイスとソフトウェア

はじめに

バーチャルキャラクターに対してどれだけプレゼンスを感じられるかというのは非常に大きな課題ですよね。
そのプレゼンスの表現において表情は非常に大きな要素です。

様々な場面・状況があり、バーチャルキャラクターに表情の概念を実装する手法も様々です。
「これ」という正解ないように思え、イベントやバーチャルキャラクターごとに工夫を凝らしているように思います。
とはいえ、ある程度よく使われる手法や組み合わせといったものはあるように感じているので、自分の知っているものを紹介していきます。
(Unityのようなリアルタイムゲームエンジン上にシステムを組む前提で、既製品のVTuberシステムは省きます。)

デバイスとソフトパターン

ヘッドマウントカメラ + Dynamixyz

ヘルメットのようなものにカメラをつけてそれで常時顔を映し、その映像から表情解析してキャラクターに反映させます。
ヘッドマウントカメラ
出典:茨城県が生み出した注目の地域密着型バーチャルキャラクター:茨城県公認Vtuber「茨ひより」

image.png
出典:Dynamixyz

長所

  • フェイストラッキングカメラの位置に依存せず動き回れる
  • トラッキングロストが少ない
  • 高い精度で多くの情報を取れることが多い

短所

  • ちょっと重い
  • 動きの邪魔
  • 視界が狭まる
  • (解析ソフトにもよるが)費用がかかる
  • リッチな情報が取れる分、モデル設定も大変になりがち

使われる場面

フェイストラッキングカメラの位置に依存せず動き回れるため、OptiTrackViconのようなリッチな光学式モーションキャプチャを使って動き回るときに有効です。
この手法を取るときは金銭的に余裕があるパターンも多いため、表情解析ソフトにDynamixyzが選定されることが多いですね。
Dynamixyzは映画製作やゲームの現場で使われるほど非常に解析性能が高いですが、お値段に関してはトレードオフなっています。

使われている実例

Vtuber「茨ひより」ちゃんはヘッドマウントカメラとDynamixyzの組み合わせであることを公開(茨城県が生み出した注目の地域密着型バーチャルキャラクター:茨城県公認Vtuber「茨ひより」)しています。
ひより
【#01】はじめまして、茨ひよりです!~自己紹介編~
非常に高い精度での表情反映を実現しながら、体全体での大きな動きも実現していて非常に魅力的ですね。

iPhoneX+ARkit FaceCapture

iPhoneX以降のTrueDepthカメラ搭載デバイスで使えるARkitのFaceCaptureでの表情解析データとVTuberシステムを通信させ、そのデータをキャラクターに反映させます。
image.png
出典:iPhone X offers a cheap alternative to mo-cap

長所

  • デバイス入手敷居が低い
  • デバイスに表情解析ソフトもついているようなものなので総合的に安い
  • 普及デバイスなのでネットに開発ナレッジが多い
  • 一般的に使い慣らされたデバイスなのでVTuberシステムを非エンジニアが使う想定に落とし込みやすい

短所

  • 頭につけるには重い
  • ディスプレイが視界を妨げる
  • カメラを机などに固定することが多く、その場合動きが制限される(固定カメラ画角が動ける範囲)
  • スマホ端末と通信する必要性がある

使われる場面

デバイスが重く大きく、頭の前につけて動き回るのは現実的ではないため、机に固定してゲーム実況や雑談のように基本的に動き回らないようなものに有効です。

使われている実例

にじ3Dではこの手法が使われていることが公開されています。
にじ3D
出典:「3Dライブ配信を、私1人で。」3Dにじさんじアプリ、にじさんじバーチャルライバーに配布開始!

にじ3D
樋口楓のにじ3Dお披露目&重大発表!!
動きの制限こそあれど、普及デバイスでここまでの精度を出せるのは最高ですよね。

WebCamera + Dlib

Dlibは機械学習ライブラリの一つで、とても手軽に顔の器官検出が可能です。
それを用いて自前で顔の特徴点検出のシステムを組んでしまうというものです。
image.png

長所

  • 実質無料

短所

  • 実装者の腕次第

※レイヤー的に低いところにあるので、長所も短所も実装次第

使われる場面

オリジナルでシステムを作りたいときや、特別にどうしても検出したい顔器官があるような場合に有効です。
一から作るのはなかなか骨が折れる開発になると思います。(勉強目的としてやるととっても楽しい。)

使われている実例

AssetStoreにてUnity向けのプラグインが販売されています。

GamePad + 独自システム

表情のトラッキングは行わず、コントローラの入力に応じて、視線を変化させます。
この手法の場合、自動ではないので手動での操作が必要で、音ゲーのごとくタイミングを合わせて操作します。
使用されるコントローラは色んな流派が存在し、PS4だったりXboxだったりSwichだったり...。
HMDを使っている場合だとそのままHMDのコントローラが表情操作のコントローラにもなりがち。

gamepad
出典:Xbox 360 コントローラー

長所

  • アニメ的表現がしやすい
  • 実装コストが低い
  • (そもそもトラッキングしていないので)動作が安定する
  • トラッキングの負担が演者にない

短所

  • 表現が表情操作者のテクニックに依存する
  • キャラ数だけ表情操作役を用意する必要がある

使われる場面

安定して動作し、モデルへの実装コストが低めなので一度切りのイベントで有効です。
また、HMDで顔が隠れたり激しい動きをしていたり、表情のトラッキングが難しい場面で有効です。

使われている実例

VRchatなど正しくですね。
コントローラを押すことで表情を変化させています。
vrchat
出典:Steam
また、表情トラッキングだけでは判別できない特殊な表情(><←こういう表情とか)を補うためにトラッキングシステムと組み合わせて使われるケースも多いです。
ヘッドマウントカメラ
出典:茨城県が生み出した注目の地域密着型バーチャルキャラクター:茨城県公認Vtuber「茨ひより」

比較まとめ

どの手法にもメリデメがあり、どれかが完全上位互換になりえることはほぼないと思います。
特に見栄えの部分に当たってはモデラーとエンジニアと演者による匠の調整によるところが大きいと思っています。
運用体制や予算、得意な分野から最適な手法を組み合わせて組んでいく感じでしょうね

ヘッドマウントカメラ
+Dynamixyz 
iPhoneX
+ARkit FaceCapture
WebCamera
+ Dlib                  
GamePad + 独自システム
初期費用                   ★☆☆☆☆ ★★☆☆☆ ★★★★☆ ★★★★★
運用コスト                   ★☆☆☆☆ ★★★☆☆ ★★★☆☆ ★★☆☆☆
実装コスト ★★☆☆☆ ★★☆☆☆ ★☆☆☆☆ ★★★★★
演者負担 ★★☆☆☆ ★★★★☆ ★★★☆☆ ★★★★★
精度 ★★★★★ ★★★★☆ ★★★☆☆ ★☆☆☆☆
演技反映度 ★★★★★ ★★★★☆ ★★★☆☆ ★☆☆☆☆

※星の数が多いほどその項目において優れている。
個人の主観的な比較であって絶対的な比較ではないのでご注意ください。

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

Unity+Cardboardで彼女召喚!

注意

自分はUnity初心者で、この記事は本当の初心者が書いています
(色々と間違っていたりおかしい点があると思います。)
また、iOS向けの記事です。
Androidでも手順は大体同じだと思われますが、保証はできません。

初めに

早く本編見たい人はここの部分は無視してくださいw

クリスマスが近く町(クソ田舎)が賑やかだ。(だから何だ)
友達は少なからずいるが、年末も近く忙しいらしくどこか遊びに行こうと思ったが無理そうだった。
自分には彼女はいない。(3次元に興味ないので作る気ナシ&工業系なので無理)
そう。今年もほぼ一人なので寂しい。

そんな事を考え出してから数日後...

そんな寂しい心を埋めてくれるものはないか...
あ、そうだ!
Unityがあるじゃん!(Unityは勉強始めてから私生活で忙しく4日で一旦やめた)
やっと見つけ出した。
最低限、視覚的にも寂しさを埋めようと考え、以前話題だったVRに興味を持った。
た、高い。学生にはキツい価格(PSVRで約3万円)
そんな中、調べているとCardboardの存在を思い出す。

100均(セリア)でCardboardが売っている情報を聞きつけ早速買いに行く。
買えた(110円)ので家に帰り、組み立ててYouTubeの動画で試してみる。
やべぇ。これ。すごい...
ここで本気モードが入る。

と、まぁこんな感じでVR計画が始まったわけですが、
Unity+CardboardについてiOS向けに書いてくださっている方が少なく、
古い情報ばかりで1週間くらい苦労したのでこの記事を書こうと思った次第です。

Xcodeのインストール

XcodeはAppStoreからダウンロードインストールしておきます。
Xcodeのインストールが終わったらXcodeを起動させます。
初回のコンポーネント類の自動インストールが始まります。
image.png
インストールが終わり、AppleIDやデバイス設定等が出来たら、Xcodeを終了させます。
詳しい説明は調べればすぐに出ると思います
投げやりで申し訳ありません。

Unityのインストール

Unity Hubか、Unity公式のページでDLできるインストーラよりインストールしてください。
自分がお勧めするバージョンは「Unity2019.2.0f1」です。
旧バージョンはUnity download archiveよりDLできます。
当時、最新だったUnity2019.2.15f1では謎のエラーでXcodeまで辿り着く事ができませんでした。(自分だけなのか...)
image.png
iOS向けにビルドするのでコンポーネント選択画面で「iOS Build Support」にチェックをしておきます。念のため「Mac Build Support」にもチェックしておくと良いでしょう。

後は指示通りインストールを進めていきます。

Unityプロジェクトの作成

Unityのインストールが終わった早速、プロジェクトの作成を行います。
今回は3Dを扱うのでテンプレートは「3D」を選択して任意のプロジェクト名を入れてプロジェクト作成をしてください。
image.png
こんな画面になったらOKです。

必要なものをダウンロード

次はこれから必要になるものをダウンロードをします。
必要なものは
・召喚したい彼女となるモデル(特に何でもいいです。自作のモデル(FBXがお勧め)でも良いです)
・GVR SDK for Unity
※注意! 使用するモデルの利用規約や禁止事項をよくお読みください。
中には、Unity等などで利用を禁止しているものもあります。

今回はユニティちゃんを召喚します!
ユニティちゃん公式サイトよりダウンロードします。
image.png

しっかりユニティちゃんライセンス条項を良く読んで同意できるようであれば下記のチェックボックスにチェックして進んでください。
色々なモデル等がダウンロードできるページが出たら、
上くらいのある「ユニティちゃん 3Dモデルデータ」の「DOWNLOAD」ボタンをクリックしてしてください。
image.png

次はGVR(Google VR) SDK for Unityをダウンロードします。
GoogleVRのGitHubより、ダウンロードします。
この記事を書いた時の最新はv1.200.1です。
image.png
画像内赤枠の部分を探し出し、クリックしてダウンロードします。

ダウンロードが終わったらUnityへ戻ります。

Unity側の設定

今度はUnityへ戻り、メニューバー =>File=>BuildSettingsをクリックして
image.png

BuidSettingsで画像の赤枠通り設定してください。
1.シーンの切り替え等は今回行わないため、「Add Open Senes」をクリックして初期に作成されたものを設定しておきます。
2.iOS向けにビルドするので、iOSをクリックしてください。
3.初期は「Release」になっているので念のため「Debug」にしておきます。
4.これらの設定が完了したら「Switch Platform」をクリックします。(多少、時間が掛かります。)
image.png

5.「Player Settings」をクリックして開きます。※今回は最低限の設定しかしません
Player Settingsが開いたら、「Other Settings」を開き、「bundle Identifier」のテキストボックス内に「com.xxxxx.xxxxxx」の形式で任意の名前を入力してください。(xの部分は自由)
image.png
今度は「Other Settings」内の「Target Device」を「iPhoneOnly」にします。
そして、「Target minimum iOS version」は「10.0」以上にしておきます。
image.png

UnityのVRサポートの有効化(?)
1.今度は「XR Settings」をクリックします。
2.「Virtual Reality Supported」横のチェックを入れます。
3.「Virtual Reality SDKs」内の「+」ボタンをクリックして「Cardboard」をクリックします。(少し時間が掛かります。)
終わったら、PlayerSettingsを閉じてエディタに戻ります。
image.png

召喚するための準備

image.png
この画面に戻ったら、先ほどダウンロードした「GoogleVRForUnity_1.200.1.unitypackage」を開く。
開いたらこんなウィンドウが出るので赤枠部分の「Import」をクリックします。(少し時間が掛かります)
image.png

次は先ほどダウンロードしたユニティちゃんのモデルをインポートします。
「UnityChan_1_2_1.unitypackage」を開きます。
先ほどと同じように「Import」をクリックします。(結構時間が掛かります)

一通りインポート出来たら、画面左上のヒエラルキー(Hierarchy)の「Create」をクリックして、3D Object=>Cubeをクリックしてオブジェクトを作成します。
Cubeが作成されたら、画面左横「Inspector」内の「Transform」の値を画像と同じように設定してください。
image.png

次に、画面下の「Project」タブに「Assets」が開いていると思います。
その中のGoogleVR=>Prefabs=>「GvrEditorEmulator」を画面左上のヒエラルキーに持っていってください。

遂に召喚!

画面下の「Project」内の「Assets」に戻ってください。
UnityChan=>Models=>unitychanをヒエラルキーに持っていっていくと召喚できると思います。
image.png
ヒエラルキーにある「unitychan」を選択後、InspectorのTransformの値を画像のように設定してください。
image.png
ヒエラルキー内にある「Main Camera」を選択して、「Scene」内のカメラアイコン付近にある三色のXYZ軸を右下にある「CameraPreview」を見ながら良い感じに動かしてください。
image.png

設定できたら、画面真ん中辺りにある再生ボタン(三角形のアイコンのやつ)をクリックしてください。
Gameタブが表示されたら、altキーを押しながらマウスを動かしてみてください。
image.png

画面が動きます。この動作は、実機に入れた際に視点の向きや傾きを変える動作をエミュレートしてます。

今回は召喚のみの記事なので一旦これで完成とします。

iPhone実機へ!

これからiPhone実機へビルド&インストールします。

ビルドの際にユニティちゃんに付属されているスクリプトのエラーが出てビルドが停止するのを回避するためにAssets=>unityChan=>Scripts=>AutoBlink.csを編集します。

AutoBlink.cs
using System.Security.Policy; //8行目を削除します

image.png

メニューバー=>File=>build And Runをクリックします。
image.png
Save Asには任意のビルドされたデータを格納するフォルダの名前を入力します。
Saveボタンを押すとプロジェクトの書き出し処理されます。(物凄く時間が掛かります)
Unity側の処理が終わると自動的にXcodeが立ち上がります。
画像内の青枠横の番号に合わせて設定してください。
image.png
設定が終わったら、PCにUSBケーブルでiPhoneを接続します。
image.png
Xcodeウィンドウ左上の「Generic iOS Device」をクリックしてPCに接続しているiPhoneを指定します。
そして、横の三角ボタンを押してビルド開始します。(ビルドなので時間ががります)
途中でPCのパスワードが求められるので入力して続行します。(この時点でiPhoneのロックを解除しておく)
ホーム画面にアプリが追加されていて、起動できない場合は、設定=>一般=>プロファイルとデバイス管理=>「Apple Development:XXXXX~」=>「Appを検証」をタップで検証を完了してください。
これで実行できると思います。

ホーム画面からアプリを開くと...
IMG_6671.PNG
この画面になったらiPhoneを横にする
IMG_6672.PNG
おお〜できました!
あぁ〜目の前にいますよ!
すごい...

最後に

比較的簡単にできました。
今回の手順ではほぼノンプログラミングでモデルの表示ができます。
VRは難しいイメージがありますが、簡単にできます。
たった110円でできました。
とても低コストで学生には本当に嬉しい限りです。
ブームは密かに去って行っていますが、これから色々なVRゲームがこのCardboardで出来るようになったら低コストで色々遊べますね。

とりあえず目の前にいるだけで何か寂しさも吹き飛んでいきそうです!

今回はAdvent Calendar初参加で、この記事は公開日前日に急いで書いたので
色々と酷い説明や間違っているところがあると思うので発見次第、修正をかけたいと思います。

参考にしたサイト

UnityでVR – GoogleVR導入 – | イケメンテックラボブログ

"error CS0234: The type or namespace name 'Policy' does not exist in the namespace 'System.Security'" エラーが発生する (Unityプログラミング)

ライセンス

image.png
この作品はユニティちゃんライセンス条項の元に提供されています

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

【Unity】ワールド座標によって三つのテクスチャを滑らかに切り替えるシェーダー

はじめに

本記事はUnity #3 Advent Calendar 201912月14日の記事です。

みなさんはステージ上で、チームによって動的にテクスチャを変えたいと思ったことはありませんか?
私はありました。

ということで、本記事では、チームによってステージのテクスチャを変えるために私が使用した方法を紹介しようと思います。(強引な導入)

方法は簡単で、通常のレンダリングに利用されるテクスチャやパラメータを3セット用意し、選択マップで3セット分のテクスチャやパラメータをブレンドするという力業なものです。

なお、本記事で説明するシェーダーは、昔からあるUnity built-in render pipelineを対象としたもので、Scriptable Render Pipeline(SRP)は対象としていません。
(SRPで使えるShader Graphを使えば本記事の内容はコードを書かずに簡単に実現できるかもしれないので、機会があったらShader Graphバージョンの記事を書くかもしれない)

確認環境

  • Unity2019.2.12f1
  • Windows10

概要

以下の画像の上段のような3つのテクスチャと、それぞれをRGBチャンネルに対応付けた下段のような選択マップを用意して……
triple_standard_shader1.png

これらのテクスチャからこのように表示されるようにしたい。
triple_standard_shader2.jpg

選択マップの赤部分に左のテクスチャが、緑部分に真ん中のテクスチャが、青部分に右のテクスチャが表示されているのが分かります。

選択マップはワールド座標におけるxz平面に対応しており、位置によってテクスチャを切り替えています。この性質からこんなことができます。
triple_standard_shader3.gif

動かしているボックスのテクスチャが位置によって変わっているのが分かると思います。

この記事ではこの動作を実現するシェーダーの実装方法について説明します。

方針

Unityでは主に以下の種類のシェーダーがあります。
より詳細な説明はNEAREAL「Unity の Shader の種類:超速Unityシェーダ入門(1)」が参考になります。

  • Surface Shader: 簡単なコードを書くだけで内部で複雑なシェーダーを生成してくれるシェーダー
  • Vertex, Fragment Shader: 独自にカスタマイズしたシェーダーを作れる。Surface Shaderでは実現できないような処理が書きたいときなどに使う
  • Compute Shader: GPUで計算を行うためのシェーダー

本記事ではそのお手軽さから、Surface Shaderを使用します。

実装

基本的なSurface Shader

まず、基本的な機能を備えた物理ベースシェーダーの実装を確認します。
StandardシェーダーでRendering ModeOpaqueに設定されている場合に相当するものです。

Surface Shaderを使用すると簡単に書くことができます。

StandardOpaque.shader
Shader "Custom/StandardOpaque" {
    Properties {
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Albedo (RGB)", 2D) = "white" {}
        _Glossiness("Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic("Metallic Scale", Range(0,1)) = 0.0
        [NoScaleOffset]_MetallicGlossMap("Metallic", 2D) = "white" {}
        _BumpScale("Normal Scale", Float) = 1.0
        [NoScaleOffset][Normal] _BumpMap("Normal Map", 2D) = "bump" {}
        [HDR] _EmissionColor("Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex("Emission", 2D) = "white" {}
        _OcclusionStrength("Occlusion Strength", Range(0.0, 1.0)) = 1.0
        [NoScaleOffset] _OcclusionMap("Occlusion", 2D) = "white" {}
    }

    SubShader {
        Tags { "Queue"="Geometry" "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        fixed4 _Color;
        sampler2D _MainTex;
        half _BumpScale;
        sampler2D _BumpMap;
        fixed4 _EmissionColor;
        sampler2D _EmissionTex;
        half _OcclusionStrength;
        sampler2D _OcclusionMap;
        half _Glossiness;
        half _Metallic;
        sampler2D _MetallicGlossMap;

        struct Input {
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutputStandard o) {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Bump Map
            fixed4 n = tex2D(_BumpMap, IN.uv_MainTex);
            o.Normal = UnpackScaleNormal(n, _BumpScale);
            // Emission
            fixed4 e = tex2D(_EmissionTex, IN.uv_MainTex);
            o.Emission = _EmissionColor * e;
            // Occlusion (合ってるかわからない)
            fixed4 oc = tex2D(_OcclusionMap, IN.uv_MainTex);
            o.Occlusion = oc * _OcclusionStrength;
            // Metallic and smoothness come from slider variables
            fixed4 m = tex2D(_MetallicGlossMap, IN.uv_MainTex);
            o.Metallic = m * _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Standard"
}

本記事の説明で必要な部分を中心に説明していきます。
本記事で説明していないSurface Shaderのより詳しい説明は、LIGHT11「【Unity】Surface Shaderの基本を総まとめ!難しい計算はUnity任せでサクッとシェーダ作成」@IT「Unityで始めるシェーダー入門」が参考になります。

まず、Propertiesブロックで、Unityのインスペクタで設定できるプロパティを定義しています。

続いて、SubShaderブロックではシェーダー本体のコードや設定が書かれています。

以下のコードは、どのようなSurface Shaderを生成するか設定しています。今回は物理ベースですべてのライトシャドウをサポートするよう設定しています。加えて、surf()を実際の処理時に呼びされるシェーダー関数として設定しています。

SurfaceShader設定
#pragma surface surf Standard fullforwardshadows

その下で、シェーダープログラム内で使用する変数を定義しています。
Propertyブロックで定義した変数はここで定義することで、プログラム内で使用可能になります。

次にInput構造体ですが、これはシェーダー関数(今回はsurf())の入力として使用されるものです。
処理対象のピクセルなどの情報が入っています。あらかじめ決められたものを定義することで、適切な値が格納されます。ここで定義しているuv_MainTexには、メインテクスチャ上のUV座標が格納されます。

そして、このシェーダーの本体であるsurf()です。上で説明した#pragma surface surf ...の部分で指定されているものです。
説明したInput型の変数を入力として受け取り、SurfaceOutputStandard型のとして受け取った変数へ出力する計算結果を格納します。
surf()内では出力変数の各値について、計算して格納しています。それぞれの詳細は本記事では触れないので、参考サイトなどを参照してください。

三種類のテクスチャを扱う

本題に入ります。
まずは、単純に前の章で使用したPropertyブロックの項目をそれぞれ3つ分用意します。

ここで、全ての項目を3つづつ用意すると、テクスチャプロパティ数がDirectX11における上限である16個を超えてしまいます。
そのため、法線マップ、金属光沢マップ、オクルージョンマップは共通のものを使用するようにしています。

また、三種類のテクスチャをどのように反映するかを選択するための、選択テクスチャ_selectionMapと選択に用いるワールド空間の領域_SelectionAreaを加え、プロパティは以下のようになります。_selectionMap_SelectionAreaについては下で説明します。

Propertiesブロック
Properties{
    [NoScaleOffset] _SelectionMap("Selection Map", 2D) = "red" {}
    _SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

    _Cutoff("Alpha Cutoff (Valid In Cutout Mode)", Range(0,1)) = 0.5

    _Color("[1] Color", Color) = (1,1,1,1)
    _MainTex("[1] Albedo (RGB)", 2D) = "white" {}
    _Glossiness("[1] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic("[1] Metallic Scale", Range(0,1)) = 0.0
    [NoScaleOffset]_MetallicGlossMap("[1] Metallic", 2D) = "white" {}
    _BumpScale("[1] Normal Scale", Float) = 1.0
    [NoScaleOffset][Normal] _BumpMap("[1] Normal Map", 2D) = "bump" {}
    [HDR] _EmissionColor("[1] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex("[1] Emission", 2D) = "white" {}
    _OcclusionStrength("[1] Occlusion Strength", Range(0.0, 1.0)) = 1.0
    [NoScaleOffset] _OcclusionMap("[1] Occlusion", 2D) = "white" {}

    _Color2("[2] Color", Color) = (1,1,1,1)
    [NoScaleOffset] _MainTex2("[2] Albedo (RGB)", 2D) = "white" {}
    _Glossiness2("[2] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic2("[2] Metallic Scale", Range(0,1)) = 0.0
    [HDR] _EmissionColor2("[2] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex2("[2] Emission", 2D) = "white" {}
    _OcclusionStrength2("[2] Occlusion Strength", Range(0.0, 1.0)) = 1.0

    _Color3("[3] Color", Color) = (1,1,1,1)
    [NoScaleOffset] _MainTex3("[3] Albedo (RGB)", 2D) = "white" {}
    _Glossiness3("[3] Smoothness", Range(0,1)) = 0.5
    [Gamma] _Metallic3("[3] Metallic Scale", Range(0,1)) = 0.0
    [HDR] _EmissionColor3("[3] Emittion Color", Color) = (0, 0, 0, 0)
    [NoScaleOffset] _EmissionTex3("[3] Emission", 2D) = "white" {}
    _OcclusionStrength3("[3] Occlusion Strength", Range(0.0, 1.0)) = 1.0
}

インスペクタに表示するデータを構造体でまとめることはできないので、単純にコピペしたのですが、もっといい方法ないですかね。

ワールド座標の選択テクスチャUV座標への変換

選択テクスチャのどの部分を参照するのかを決めるために、表示しようとしている点のワールド座標を選択テクスチャ上の座標に変換します。

まず、選択テクスチャがワールド座標においてカバーしている範囲の情報が必要になります。その情報の指定を_SelectionAreaで行うことができます。

_SelectionAreaは、XZ平面で選択テクスチャがカバーする矩形をx座標とz座標の最大値最小値として保持します。

SelectionSreaプロパティ
_SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

次に、計算対象となる点のワールド座標が必要です。
これは入力構造体の定義に以下の行を追加することで使用可能です。

ワールド座標定義
struct Input {
    float2 uv_MainTex;
    float3 worldPos; // これ
};

以上の二つを使用して、選択マップにおける座標は以下のコードで計算できます。

座標変換
float2 calculate_selection_map_uv(float3 world_pos, float4 selection_area){
    float2 uv;
    uv.x = (world_pos.x - selection_area.x) / (selection_area.z - selection_area.x);
    uv.y = (world_pos.z - selection_area.y) / (selection_area.w - selection_area.y);
    return uv;
}

戻り値は、選択マップ上の位置を表す、それぞれの要素が0から1であるfloat2型です。

選択マップの反映

上で求めた参照位置における、選択マップの色情報を以下のコードで取得します。αチャンネルは使用しないので、fixed3型で受け取ることで無視します。

SelectionMapの色情報取得
float2 selection_map_uv = calculate_selection_map_uv(IN.worldPos, _SelectionArea);
fixed3 sm = tex2D(_SelectionMap, selection_map_uv);

smは各チャンネルがそれぞれ赤、緑、青を表す、値域が0から1の色情報を格納した三次元ベクトルです。
3つのテクスチャをsmの各チャンネルの値で重み付けして加算し、得られた値を最終出力とします。

この動作を実現するために、surf_core()を以下のように定義します。
surf_core()は各チャンネルでの出力値を計算し反映する関数です。

surf_core関数
void surf_core(Input IN, MaterialSetting ms, fixed ratio, inout SurfaceOutputStandard o) {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D(ms.MainTex, IN.uv_MainTex) * ms.Color;
    o.Albedo += c.rgb * ratio;
    // Bump Map
    fixed4 n = tex2D(ms.BumpMap, IN.uv_MainTex);
    o.Normal += UnpackScaleNormal(n, ms.BumpScale) * ratio;
    // Emission
    fixed4 e = tex2D(ms.EmissionTex, IN.uv_MainTex);
    o.Emission += ms.EmissionColor * e * ratio;
    // Occlusion (合ってるかわからない)
    fixed4 oc = tex2D(ms.OcclusionMap, IN.uv_MainTex);
    o.Occlusion += oc * ms.OcclusionStrength * ratio;
    // Metallic and smoothness come from slider variables
    fixed4 m = tex2D(ms.MetallicGlossMap, IN.uv_MainTex);
    o.Metallic += m * ms.Metallic * ratio;
    o.Smoothness += ms.Glossiness * ratio;
    o.Alpha += c.a * ratio;
}

基本的には基本的なSurface Shaderに載せたコードのsurf()関数と同じです。
違いは、比率ratioを受け取り、各要素にratioを掛けた値を加算するようになっている点です。

引数で使用されているMaterialSetting構造体は、入力変数をまとめたものです。

MaterialSetting
struct MaterialSetting {
    fixed4 Color;
    sampler2D MainTex;
    half BumpScale;
    sampler2D BumpMap;
    fixed4 EmissionColor;
    sampler2D EmissionTex;
    half OcclusionStrength;
    sampler2D OcclusionMap;
    half Glossiness;
    half Metallic;
    sampler2D MetallicGlossMap;
};

値を設定するのではなく加算していくので、一番初めに出力構造体の要素を全て初期化する必要があります。

このsurf_core()surf()ないで、_SelectionMapの3チャンネル分呼び出します。
これで、最終的には_SelectionMapの各チャンネルの値に応じて、3つの設定によるしゅつりょkぐあブレンドされた出力が得られます。

全体の実装

ソースコードのファイルは以下の二つになっています。

  • TripleBlendStandardOpaque.shader: StandardShanderの設定とTripleBlendStandardCore.cgincのインクルード
  • TripleBlendStandardCore.cginc: 選択マップによるテクスチャの加算

cgincファイルはほかのシェーダープログラムからC言語のように#includeすることができるファイルで、同じ処理や変数のコード共通化などに活用できます。

TripleBlendStandardOpaque.shader
Shader "Triple/Standard-Opaque"{
    Properties{
        [NoScaleOffset] _SelectionMap("Selection Map", 2D) = "red" {}
        _SelectionArea("Selection Area (MinX, MinZ, MaxX, MaxZ)", Vector) = (0, 0, 100, 100)

        _Cutoff("Alpha Cutoff (Valid In Cutout Mode)", Range(0,1)) = 0.5

        _Color("[1] Color", Color) = (1,1,1,1)
        _MainTex("[1] Albedo (RGB)", 2D) = "white" {}
        _Glossiness("[1] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic("[1] Metallic Scale", Range(0,1)) = 0.0
        [NoScaleOffset]_MetallicGlossMap("[1] Metallic", 2D) = "white" {}
        _BumpScale("[1] Normal Scale", Float) = 1.0
        [NoScaleOffset][Normal] _BumpMap("[1] Normal Map", 2D) = "bump" {}
        [HDR] _EmissionColor("[1] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex("[1] Emission", 2D) = "white" {}
        _OcclusionStrength("[1] Occlusion Strength", Range(0.0, 1.0)) = 1.0
        [NoScaleOffset] _OcclusionMap("[1] Occlusion", 2D) = "white" {}

        _Color2("[2] Color", Color) = (1,1,1,1)
        [NoScaleOffset] _MainTex2("[2] Albedo (RGB)", 2D) = "white" {}
        _Glossiness2("[2] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic2("[2] Metallic Scale", Range(0,1)) = 0.0
        [HDR] _EmissionColor2("[2] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex2("[2] Emission", 2D) = "white" {}
        _OcclusionStrength2("[2] Occlusion Strength", Range(0.0, 1.0)) = 1.0

        _Color3("[3] Color", Color) = (1,1,1,1)
        [NoScaleOffset] _MainTex3("[3] Albedo (RGB)", 2D) = "white" {}
        _Glossiness3("[3] Smoothness", Range(0,1)) = 0.5
        [Gamma] _Metallic3("[3] Metallic Scale", Range(0,1)) = 0.0
        [HDR] _EmissionColor3("[3] Emittion Color", Color) = (0, 0, 0, 0)
        [NoScaleOffset] _EmissionTex3("[3] Emission", 2D) = "white" {}
        _OcclusionStrength3("[3] Occlusion Strength", Range(0.0, 1.0)) = 1.0
    }

    CGINCLUDE
    #include "TripleBlendStandardCore.cginc"
    ENDCG

    SubShader{
        Tags { "Queue" = "Geometry" "RenderType" = "Opaque" }
        LOD 200
        CGPROGRAM

        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        ENDCG
    }

    Fallback "Standard"
}
TripleBlendStandardCore.cginc
#ifndef TRIPLE_STANDARD_CORE_CGINC_INCLUDED
#define TRIPLE_STANDARD_CORE_CGINC_INCLUDED

sampler2D _SelectionMap;
float4 _SelectionArea;

fixed4 _Color;
sampler2D _MainTex;
half _BumpScale;
sampler2D _BumpMap;
fixed4 _EmissionColor;
sampler2D _EmissionTex;
half _OcclusionStrength;
sampler2D _OcclusionMap;
half _Glossiness;
half _Metallic;
sampler2D _MetallicGlossMap;

fixed4 _Color2;
sampler2D _MainTex2;
fixed4 _EmissionColor2;
sampler2D _EmissionTex2;
half _OcclusionStrength2;
half _Glossiness2;
half _Metallic2;

fixed4 _Color3;
sampler2D _MainTex3;
fixed4 _EmissionColor3;
sampler2D _EmissionTex3;
half _OcclusionStrength3;
half _Glossiness3;
half _Metallic3;

struct Input {
    float2 uv_MainTex;
    float3 worldPos;
};

struct MaterialSetting {
    fixed4 Color;
    sampler2D MainTex;
    half BumpScale;
    sampler2D BumpMap;
    fixed4 EmissionColor;
    sampler2D EmissionTex;
    half OcclusionStrength;
    sampler2D OcclusionMap;
    half Glossiness;
    half Metallic;
    sampler2D MetallicGlossMap;
};

float2 calculate_selection_map_uv(float3 world_pos, float4 selection_area){
    float2 uv;
    uv.x = (world_pos.x - selection_area.x) / (selection_area.z - selection_area.x);
    uv.y = (world_pos.z - selection_area.y) / (selection_area.w - selection_area.y);
    return uv;
}

void surf_core(Input IN, MaterialSetting ms, fixed ratio, inout SurfaceOutputStandard o) {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D(ms.MainTex, IN.uv_MainTex) * ms.Color;
    o.Albedo += c.rgb * ratio;
    // Bump Map
    fixed4 n = tex2D(ms.BumpMap, IN.uv_MainTex);
    o.Normal += UnpackScaleNormal(n, ms.BumpScale) * ratio;
    // Emission
    fixed4 e = tex2D(ms.EmissionTex, IN.uv_MainTex);
    o.Emission += ms.EmissionColor * e * ratio;
    // Occlusion (合ってるかわからない)
    fixed4 oc = tex2D(ms.OcclusionMap, IN.uv_MainTex);
    o.Occlusion += oc * ms.OcclusionStrength * ratio;
    // Metallic and smoothness come from slider variables
    fixed4 m = tex2D(ms.MetallicGlossMap, IN.uv_MainTex);
    o.Metallic += m * ms.Metallic * ratio;
    o.Smoothness += ms.Glossiness * ratio;
    o.Alpha += c.a * ratio;
}

void surf(Input IN, inout SurfaceOutputStandard o) {
    float2 selection_map_uv = calculate_selection_map_uv(IN.worldPos, _SelectionArea);
    fixed3 sm = tex2D(_SelectionMap, selection_map_uv);

    o.Albedo = 0;
    o.Normal = 0;
    o.Emission = 0;
    o.Occlusion = 0;
    o.Metallic = 0;
    o.Smoothness = 0;
    o.Alpha = 0;

    MaterialSetting ms;
    ms.Color = _Color;
    ms.MainTex = _MainTex;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor;
    ms.EmissionTex = _EmissionTex;
    ms.OcclusionStrength = _OcclusionStrength;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness;
    ms.Metallic = _Metallic;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.r, o);

    ms.Color = _Color2;
    ms.MainTex = _MainTex2;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor2;
    ms.EmissionTex = _EmissionTex2;
    ms.OcclusionStrength = _OcclusionStrength2;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness2;
    ms.Metallic = _Metallic2;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.g, o);

    ms.Color = _Color3;
    ms.MainTex = _MainTex3;
    ms.BumpScale = _BumpScale;
    ms.BumpMap = _BumpMap;
    ms.EmissionColor = _EmissionColor3;
    ms.EmissionTex = _EmissionTex3;
    ms.OcclusionStrength = _OcclusionStrength3;
    ms.OcclusionMap = _OcclusionMap;
    ms.Glossiness = _Glossiness3;
    ms.Metallic = _Metallic3;
    ms.MetallicGlossMap = _MetallicGlossMap;
    surf_core(IN, ms, sm.b, o);
}

#endif // TRIPLE_STANDARD_CORE_CGINC_INCLUDED

注意しないといけないのは、使用するテクスチャ枚数が2倍以上になっているため使用するビデオメモリの量も2倍以上になる点です。

使用方法

マテリアルのインスペクタで以下のように設定します。
inspector_setting.jpg

すると、初めに見せた画像のような状態になります。

triple_standard_shader3.gif

応用例

現在制作しているゲームで使用している、本記事で書いた方法の使用例を載せておきます。

現在作成しているゲームでは、ステージ内にタワーが配置されており、タワーは「影」「光」「中立」チームのいずれかが保持しています。
タワーの周辺はそのチームの領域になり、それによってステージの見た目が変化する仕組みになっています。

まずは、【Unity】コンピュートシェーダーを使ってボロノイ図のビットマップを生成するの記事で説明した方法で、チーム領域ごとの選択マップを生成します。緑が光、青が影、赤が中立チームです。それを使用して、本記事で説明した方法でステージ全体をチームごとに色分けします。
application1.jpg

結果的にこんな感じになります。
application2.jpg

タワーのチームが変わると、それに応じてリアルタイムに周辺の地形の色が変化します。

その他

ここで説明したソースコードは以下のリポジトリに置いてあります。

GitHub「unity-triple-blend-shader」

今回説明したシェーダーは、StandardシェーダーにおけるRendering ModeOpaqueに設定されている場合に当てはまるものですが、ほかの3つのモードに対応するシェーダーもリポジトリに置いてあります。
また、選択マップを実行時に設定するためのスクリプトも用意してあります。

参考文献

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

UnityでVue.js風のリアクティブシステムを作ってみる

はじめに

こんにちは、QualiArts Advent Calendar 2019、14日目の記事になります。

直近はUnityの開発していますが、その前はずっとウェブのフロントをやっていたため、本記事はVue.jsの(個人的に)目玉機能であるリアクティブシステムをUnityで再現してみるという話について書きます。

※日本語がネイティブではなくて、文書が読みづい可能性がありますので、御了承してください :smiley:

Vue.jsのリアクティブシステム

Vue.jsの事が知らない方に簡単の例↓を出します。詳しく知りたい方はVue.js公式でどうぞ

var app = new Vue({
  el: '#app',

  data: {
    familyName: "佐藤",
    givenName: "太郎"
  },

  computed: {
    fullName() {
      console.log('[実行されたよ]');
      return this.familyName + this.givenName;
    }
  }
});

console.log(app.fullName);
// [実行されたよ]
// 佐藤太郎

app.familyName = '鈴木';
console.log(app.fullName);
// [実行されたよ]
// 鈴木太郎

console.log(app.fullName);
// 鈴木太郎

dataに入る物は全てリアクティブプロパティであり、computedに入る物は算出プロパティであります

リアクティブプロパティ:アクティブデータの源泉であり、値の更新が監視できます
算出プロパティ:基本的に呼び出さない限り実行されない、かつ依存しているリアクティブプロパティが変わらない限り再実行もされない仕様であります

裏には遅延処理とキャッシュが自動的に行われるため、使う側は直感的なコードが書けて、無駄の再実行も自動的に管理してくれる素晴らしい機能です:zap:

Unityのプロパティ周り(C#)

C#言語にはプロパティというのあります、上記の機能をざっくり再現してみましょう

public class App
{
    public string familyName = "佐藤";
    public string givenName = "太郎";

    string _fullName;
    public string FullName
    {
        get
        {
            var value = familyName + givenName;
            if (value != _fullName)
            {
                _fullName = value;
            }
            return _fullName;
        }
    }
}

厳密に一緒ではないですが、familyName + givenNameの再実行は避けれない事になります。例えば、
familyNamegivenNameの更新フラグを追加し、それぞれの更新チェックをする処理をすれば実現できますが、コード量はおそらく2倍になるでしょう。

その他のやり方

  • 上記の比較・キャッシュ処理をメソッド化し、コードを簡略する
  • INotifyPropertyChanged プロパティ更新のイベント発火で検知する
  • UniRxのReactiveProperty プロパティをObservable化して、更新購読する
  • など

欲望

色んなやり方は既にありますが、せっかくなのでVue.jsに近い形で再現できないかというのはきっかけです。例えば↓のような感じです。

public class App
{
    public string familyName = "佐藤";
    public string givenName = "太郎";

    public string FullName => familyName + giveName;
}

IL修正 との出会い

色々調べていた所、INotifyPropertyChangedのinterface実装を自動化したライブラリー (Fody/PropertyChanged)を見つけました。
Fody自体はMono.cecilのラッパーで、アセンブリのILを修正したりできるライブラリーです。
これを使えばできるじゃないかと思ってissues調べたら、どうやらUnityは未対応のようです。

むむ、もうちょっとググったり、github内検索したりしてみたら、Unityに対応するライブラリー (ByronMayne/Weaver)が出てきました。ちゃんとUnityのコンパイル後にフックで実行されますので、これを採用する事にしました。

プロトタイプを作ってみる

IL周りを実装する前に、Vue.jsのリアクティブ実装を参考しながら、C#のベースクラスを作成してみました。

ざっくりの原理

  1. あるWatcher(A君)がいます。
  2. このA君があるWatcher(B君)に依存して、B君の値を取得します。
  3. この時、B君が依存する他のWatcher(C君)がいれば、A君もC君に依存するような関係性を持たせるようにします。
  4. 依存がなくなるまで再帰的に続きます。

この原理に基づいてプロトタイプを作成しました。

IL修正 でコード簡略化する

その後、[Reactive][Computed]2種類のAttributeを用意し、AttributeをつけたらWatcherのインスタンスの生成処理やGetter/Setterの処理委託をILで自動挿入するようにしました。

public class App
{
    [Reactive]
    public string FamilyName { get; set; } = "佐藤";

    [Reactive]
    public string GivenName { get; set; } = "太郎";

    [Computed]
    public string FullName => FamilyName + GivenName;
}

↑のコードはIL挿入後、↓という風に展開されます

public class App
{
    Wacther<string> _familyName;
    public string FamilyName
    {
        get
        {
            if (_familyName == null) 
            {
                _familyName = new Watcher<string>();
            }
            return _familyName.Get();
        }
        set
        {
            if (_familyName == null) 
            {
                _familyName = new Watcher<string>();
            }
            _familyName.Set();
        }
    }

    Wacther<string> _givenName;
    public string GivenName
    {
        get
        {
            if (_givenName == null) 
            {
                _givenName = new Watcher<string>();
            }
            return _givenName.Get();
        }
        set
        {
            if (_givenName == null) 
            {
                _givenName = new Watcher<string>();
            }
            _givenName.Set();
        }
    }

    Wacther<string> _fullName;
    public string FullName
    {
        get
        {
            if (_fullName == null) 
            {
                _fullName = new Watcher<string>(() => 
                {
                    return FamilyName.Get() + GivenName.Get();
                });
            }
            return _fullName.Get();
        }
    }
}

これで動ける最低限のプロトタイプは完成しましたが、実用までにはまだ改善と追加機能が必要です。必須的な機能例:
・ Setterの通知をまとめて次のTickで行う(重複実行を防ぐ、循環参照の検知)
・ WatcherにIDをつけて、通知はID順で実行する(実行順番の保証)

終わりに

Viewにバインディングする機能はまだないので、実質全然使えないプロトタイプですが、UnityでもVue.js風の書き方ができそうという事が証明できたじゃないかなと思っています。

C#初心者だったので、IL周りの試行錯誤を通してC#の裏はこんな感じなんだを知れたし、コンパイラはいかに便利かも知れたので、個人的に面白い体験と思いました。

上記のプロトタイプ実装はgithubに公開していますので、興味ある方ぜひこちらどうぞ。
https://github.com/thammin/kaki-watcher

以上、14日目の記事でした。引き続き今後のカレンダー投稿を宜しくお願いします。

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

【Unity】 ネットワーク

初めに

・この記事は書籍「Unity5 オンラインゲーム開発講座」をまとめていく。

オンラインゲームとは

通信の分類

リアルタイム同期

・キャラクターの移動や協力プレイなどリアルタイムな同期が必要

非リアルタイム同期

・ランキングの結果や他ユーザのユニットをお供に連れていく場合など、特定のタイミングのみ通信を必要とするもの。

ターンベース制

・オセロや将棋とか、シャドウバースとか

非ターンベース制

・RPGやアクションなど

MO

・数人~数十人が一つの空間に集まりゲームが展開される。
・マリカ

MMO (Massively Multiplayer Online)

・数百人~数千人が一つの空間に集まりゲームが展開される。
・FF14

メリット・デメリット

メリット

・他ユーザとの交流(チャットや協力プレイなど)の楽しみが生まれる。
・他ユーザとの対戦:CPUでは得られない勝利の喜び
・シングルプレイモードを手抜きすることもできる。(CPUのAIやストーリーの作成など)

デメリット

・プレイヤ数が必要
・ユーザ対応が必要
・テストの工数の増加、テストパターンの複雑化。
・サーバ維持コストが必要

課金の種類

・有料販売 : ネームバリューがないと難しい
・アプリ内課金 : 一番現実的
・広告収入
・リワード広告

開発環境

環境一覧

・Unity 2019.2.9f1
・Photon RealTime
・Photon Unity Network (Asset)

Photon Realtime

概要

・Exit Games社が開発したネットワークエンジンと呼ばれるシステム
・Android, iOS, ブラウザなどのプラットフォーム向け。
・リアルタイム、マルチプレイヤー、マッチメイキングに対応している。
・クラウドシステム(SaaS)
・デフォルトの同時ユーザ接続数(CCU)は 20
・ロードバランシングAPIを利用するとプラットフォームを問わず通信できる。

環境準備

① アカウントを登録する。

・リンク : https://www-jp.exitgames.com/ja/Realtime
キャプチャ222.PNG
・無料プランはこちらのボタンを押下。
・メールを入力
・受信したメールからアカウントを登録する。

② Photon Cloud アプリケーションを作成する。

キャプチャ.PNG
・新しくアプリを作成する。を押下する。
キャプチャ.PNG
・Photonの種別を「PUN」として作成するを押下する。
キャプチャ.PNG
・作成したものがアプリケーション一覧に表示されていればOK

③ SDKをダウンロード

キャプチャ.PNG
・UnityのAsset Storeから「Photon Unity Networking Free」をインポートする。
キャプチャ.PNG
・アプリケーションIDの入力が求められるので、②で作成したもののIDをコピペする。

・Closeを押下する。

④サーバ設定

キャプチャ.PNG
・プロジェクトビューからPhoton Unity Networking -> Resources -> PhotonServerSettingsを選択しインスペクタからサーバの設定ができる。

項目 説明
AppId アプリケーションID
Regioin どのエリアのサーバを使用するか。

Photonを使ったネット接続

全体の概要

・PUN(Photon Unity Network, Unity側で利用できるSDK)を用いて、クラウドのサーバへアクセスする。
・NameServer -> GameServer(Lobby) -> MasterServer(Room)の順に接続する。
・部屋の作成や入退出などの処理に対応したコールバック関数(On~~~)が用意されているため、それをオーバーライドする。
 (いちいち、〇〇のフラグが経つまでupdate()で監視するといった面倒な処理がないので便利)
・下図はあくまでイメージ(シーケンス図の記法は守られてないです。)

サーバの種類

・PUNは3種類のサーバを使用する。

NameServer

・指定リージョンのマスターサーバのIPアドレスを取得する。
・リージョンはコンフィグで設定したリージョンのこと。

MasterServer

・ロビーの役割を担うサーバ
・マッチメイキングに対応。(どのユーザ同士をマッチさせるかなど)
・スクリプトからは、ルームの一覧情報を取得できる。(ロビーにいる間有効)

GameServer

・ルームの役割を担うサーバ
・ルーム内のプレイヤにメッセージを伝達する。
・ルーム内のプレイヤ情報を取得する。(ルームにいる間有効)

クライアントの種類

・クライアントの種類には「一般クライアント」と「マスタークライアント」がある。

マスタークライアントの条件

・そのルームに一番に入室しているプレイヤーがマスターに割り当てられる。
・マスタークライアントが退出すると、ルーム内の他のプレイヤー一人がマスタークライアントに切り替わる。

マスタークライアントの役割

・プレイヤーの強制退出
・自身のマスター権限の譲渡 (退出した場合は自動でこれが行われている?)
・スクリプト上でマスターかを判定して、分岐処理
 (例.ゲームのリザルトを他プレイヤに送信する。)

コールバック関数

メソッド 説明 トリガー 備考
OnConnectedToMaster ロビーへ入室した ConnectUsingSettings コンフィグのautoJoinLobbyが=false
OnJoinedLobby ロビーへ入室した ConnectUsingSettings コンフィグのautoJoinLobbyが=true
OnJoinedRoom ルームの入室に成功 JoinRandomRoom
OnPhotonPlayerConnected 自分以外がルームに入室 JoinRandomRoom
OnPhotonRandomJoinFailed ルームの入室に失敗 JoinRandomRoom
OnCreatedRoom ルームの作成に成功 CreateRoom
OnPhotonRandomCreateFailed ルームの作成に失敗 CreateRoom
OnLeftRoom ルームを退出した LeaveRoom
OnApplicationPause(bool) スリープ/スリープ解除した スマホのスリープ Unity起動時かサーバ接続時かにスリープ解除(false)扱いでコールバック関数が呼ばれることに注意

ステータス変数

PhotonNetworkクラスには便利なstaticフィールドがいっぱい。

変数名 説明
connected いずれかのサーバに接続できているかどうか。
insideLobby  ロビーにいるかどうか。
inRoom 入室中であるかどうか。 (ClientState.Joinedと同じ。)
connectionStateDetailed PhotonNetwork.ClientStateのenum型。接続開始から、どのサーバに接続中か、ルームに入室した、退出したなど細かいステータス
isMasterClient マスタークライアントかどうか
room 入室中の部屋情報。入室人数など
player, playerList, otherPlayers 入室中のプレイヤの情報。プレイヤ名など

データ連係

プレイヤー間でデータの連係をする方法には2種類ある。

RPC (Remote Procedure Call)

定義
    [PunRPC]
    public void SetGameState(int gameState) {
        this.gameState = (GameState)gameState;
    }
呼び出し
  PhotonView myView = GetComponent<PhotonView>();
  myView.RPC("SetGameState", PhotonTargets.All, new object[] { (int)GameState.Judge });

手順

・データの通信を行う「PhotonView」コンポーネントをアタッチする。

説明

・メソッド呼び出し時のみ通信を行うため、負荷が小さい。

・第1引数にはメソッド名、第2引数には伝搬対象、第3引数はメソッドの引数
・伝搬対象には以下がある。
 - All : 自分を含む全員
 - Others : 自分以外の全員
 - MasterClient
・第3引数はシリアライズ可能でないといけないっぽい。(そのままGameState(enum型)で渡すとパラメタ不整合のような内容のエラーとなる。)

OnPhotonSerializedView

public class ClickCounter : Photon.MonoBehaviour, IPointerClickHandler
{
    private int counter1;
    private int counter2;
    void Start() {
        counter1 = 0;
        counter2 = 100;
    }

    public void OnPointerClick(PointerEventData eventData) {
        counter1++;
        counter2--;
        t.text = counter1.ToString() + "/" + counter2;
    }

    // OnPhotonSerializeViewによりデータの送受信を行う。
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) {
        // データを送信する。
        if (stream.isWriting)
        {
            // 送りたい分だけ記載する。
            stream.SendNext(counter1.ToString());
            stream.SendNext(counter2.ToString());
        }
        // データを受信する。
        else
        {
            counter1 = int.Parse(stream.ReceiveNext().ToString());
            counter2 = int.Parse(stream.ReceiveNext().ToString());
            t.text = counter1.ToString() + "/" + counter2.ToString();
        }
    }
}

手順

・データの通信を行う「PhotonView」コンポーネントをアタッチする。
・PhotonViewのObserved Componentsにデータ連係したいコンポーネントを設定する。

説明

・常時通信を行うため、負荷が大きい。
・マスタークライアントの場合、一定時間ごとにメソッドが呼ばれ、データ送信のみ行う。
・一般クライアントの場合、マスター側で値の変更があった場合のみメソッドが呼ばれ、データ受信のみ行う。
 逆に一般クライアントの変更をトリガーにデータ送信は行われなかった。
・伝搬対象は「PhotonTargets.Others」

インスタンス連係

 PhotonNetwork.Instantiate(prefub.name, new Vector3(9.0f, 0f, 0f), Quaternion.identity, 0);

・プレハブから生成したインスタンスが他のクライアントにも生成される。
・プレハブは「Resources」配下に置く必要がある。

カスタムプロパティ

・各クライアントまたはルームに対して保存できるハッシュテーブル。
・データはサーバに保存される。
・頻繁にRPCなどによるデータ伝搬を行わなくても済む。
・プレイヤの戦績情報など、変化しえない情報の格納に使えそう。

  // データの初期化
  ExitGames.Client.Photon.Hashtable playerHash;  
  playerHash = new ExitGames.Client.Photon.Hashtable();
  // データの追加
  playerHash.Add("test1", 30);
  // データの保存
  PhotonNetwork.player.SetCustomProperties(playerHash);
  // データの参照
  int value = (int) PhotonNetwork.player.CustomProperties["test1"];

  // ルームについては、PhotonNetwork.playerがPhotonNetwork.roomに置き換わるだけ。

トラブルシューティング

PUNに関するエラーとその対処方法を以下にまとめる。

AppId未設定

The appId this client sent is unknown on the server (Cloud). Check settings. If using the Cloud, check account.

原因 : 正しいAppIdが設定されていない。
対処 : マイページのアプリケーションから正しいAppIdをコピペする。

プレハブの作成タイミング

Failed to Instantiate prefab: Cube. Client should be in a room. Current connectionStateDetailed: JoinedLobby UnityEngine.Debug:LogError(Object)
NullReferenceException: Object reference not set to an instance of an object

原因 : 共通するプレハブは"ルーム内"でしか使用できない。

ルーム接続時エラー

Debug.LogError("JoinRandomRoom failed. Client is not on Master Server or not yet ready to call operations. Wait for callback: OnJoinedLobby or OnConnectedToMaster.");

原因/対処 : ロビー接続前にルーム接続を行ってはいけない。(動作的にはエラーを吐くものの正常にルームには入れてしまう。)

残分

〇true : OnJoinedLobbyが呼ばれる, false OnConnectedToMasterが呼ばれる。
双方の違いについては、そもそもMasterが何かを学ぶ必要がある。
〇カスタムプロパティなRoomの作成とランク別マッチ
〇根本、PUNを使用しないUnityで利用できるネットワークの機能を理解していない。

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

Unityで実装するクラシックなゲームの制御方法

自己紹介

2008年度、数値班に所属していたMachiaWorxと申します。
なんか面白そうなアドベントカレンダーがあったのでせっかくだから参加しました。

(コミケとかで隣のサークルさんとSTGの実装について話するとやっぱ同じことするよね・・・って話題になるんだけど、そこらへん共有されてないので書き記しとくのも目的です)

概要

Unity上でクラシックなゲームを実装するための方法について連携します。

クラシックなゲーム=更新するデルタタイムの長さを固定に近づけるというのが大前提になります。(更新状況により正確には固定にならんケースもあるので)
それに付随するように各機能を実装するという方針で話を進めます。
余談ですが、上記状況はどちらかというとVSyncを前提としてゲームを作るという形を取ってたからだとは思うんですけどねえ・・・。

簡単に言うとUnityだとモダンな作りになっていて、クラシックなゲームっぽく調整しようとすると無理が出てきます。
よって、Unity上で実装を工夫していくことでクラシックなゲームっぽい動きを再現する形ですね。

どちらかというとシングルスレッドっぽく動かすため、マルチスレッドで動くUnity本来の動きと比較して性能面の劣化は免れないことに注意してください。

(実際のところ2000年代のDirectXを直接叩いてたゲームとかは割とシングルスレッドでの制御になってたんじゃないかと。VSYNCすると勝手に120FPSで動作する時どうしたらいいかと悩んでた世代とか・・・)

得られる知見

  • Unityにおける動作とゲームエンジン使わないケースでの処理の違い
  • 各処理の自前実装方法

割とUnityゴリゴリ使ってる人向けになっちゃうかもしれませんが、いざとなったら自分で実装できちゃうよ!みたいなのが分かるので、エンジン使う前に色々調べてる人向けともいえるかも。

Unityのバージョンは、Unity 2018.2.18f1 を利用してますが、
コミケが終わったら更新予定です。

基本的なアプローチ

フレーム制御を固定化する

どういう事かと言うと、「Update処理に対して各関数を動かさない」。

  • 神GameObjectを作成(各Objectに動作命令を出すためのGameObject)
  • 神Objectでは経過フレーム管理を行い、0.01666666秒ごとに処理を行う形にする
  • 各オブジェクトは神Objectに関連付けて動作を行うようにする。このためUpdate処理では処理を実行しない。(別の関数で代替する)

影響は以下の通り。

  • Update関数を利用することを前提とした処理全般が実質利用不可能となる。(利用してもいいけど実際の時間軸と神Objectの時間管理にズレがある場合、違和感が出たり後述の当たり判定にズレが出る)

ただこの方法の利点は、「神Objectから各Objectに更新処理を行うように制御をかける」ため、「リプレイを実装可能」ということ。
通常だとUpdate処理がマルチスレッドで走ることになるため、リプレイの実装などということがそもそも不可能ですが、今回の方法だと神Objectをトリガーとして各処理を動かすため、処理タイミングの制御がしやすくなります。

よって、アクションゲーム・シューティングゲームにおけるリプレイの実装もやりやすくなります。(ランダムシードの固定化、デモシーンのフレーム制御実装等は必要かと思いますが)

ちなみに、FixedUpdate関数で処理するのも問題ないとは思いますが、FixedUpdate自体の動作が信用ならんときがあるので、頼り切りにするのもどうかと思います。(FixedUpdate自体が遅延するケースとか。ゲームの内容としては問題ないとおもうんですが、特定アセットでUpdateでの処理を前提にした場合動作にズレが出ます)

Update内のフレーム制御・FixedUpdate・VSync対応みたいな形で場合分けできればベストかと。

(自分の場合はVSyncを切るの前提でUpdate関数でフレーム管理するのをデフォルトにしてます。Unityのアセットを使った動画撮影時に影響が出にくいため)

Collider同士の衝突を当たり判定の検出として使いたくない

表題の通りです。
具体的には、判定の検出にCollider同士の衝突を使わないようにしたいです。

理由は、神Objectのフレーム管理との同期が取れなくて、意図しないタイミングで当たり判定の検出が行われるのを避けるため。

とは言ってもいきなり自前当たり判定検出も難しいと思うので、UnityのCollider機能を利用した判定検出・完全に自前判定検出という形でアプローチを書いていきます。

初期の実装(Unityの機能を使った判定検出)
  • OnCollisionEnterなどで検出した際は、神Objectが処理を行うまで待つ形にし、判定フラグだけを立てておく。神Objectで更新関数を処理したタイミングでフラグ検出を行い、立っていたらダメージ判定処理を行う

この方法の問題点は、神Objectと非同期で処理を行うため、あるタイミングでは判定をすり抜ける現象が発生したり、処理タイミングが毎回異なる形になります。
それでも違和感はだいぶ減ると思うので、判定検出の実装が難しそうならこの方法をおすすめします。
(あと基本的には1/60秒より早いタイミングで衝突判定が発生すると思うのでこのやり方で問題でるケースは割と少ないはず)

後期の実装(完全に自前実装)
  • 神Object経由で更新処理を行う際、当たり判定を検出する すなわち、当たり判定の検出処理を自分で作成する

ここまでする場合、もはやUnityで処理をする意義は薄いといえます。
レンダリングだけUnityでやってるような状況とも言えてしまうので。

一番単純なのは、座標を軸にして矩形で判定取ってしまう方法です。
当たり判定でググればいっぱい出てきます。
3Dの判定の場合直方体の判定検出等ステップアップで考えれば問題はないはず。
自分の場合矩形+奥行きの判定を複数入力できるようにしてます。
計算を省力化できるなら矩形だけでいいかもしれません。

ただ、Colliderを使った処理のうちRayCast関数は任意のタイミングで呼び出すことができるので、判定検出の一環でRayCastを使って敵弾等の判定には自前での処理、という形で役割分担が可能です。
(勿論上記の場合、Collider+自前当たり判定の設定が必要になりますが)

あと、判定すり抜けは構造上起きてしまうので、自前でやるなら前フレームの判定と比較する等も仕込むことが可能です(その分重くなるけど。あとそこまでの精度が必要かという部分は要検討)

UnityのAnimation関数をなんとか使いたい

上記の通り、Update関数を前提として動く処理全般は利用不可能になるため、UnityにおけるAnimationファイルの処理が利用不可能になるのが現状。

ただ、それでもなんとかしたい場合、フレーム制御に紐づけての処理は可能。

https://taskem1985.tumblr.com/post/122781367127/%E3%82%A2%E3%83%8B%E3%83%A1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%86%8D%E7%94%9F%E4%BD%8D%E7%BD%AE%E3%82%92%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%A7%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B

上記URLの方法が一番手っ取り早いんですが、UnityのAnimationをいじらなきいけないのでちょい面倒です。

で、内部でPlayableAPIを利用した、SimpleAnimationという公式アセットが存在するのでそれ使うという手があります。

SimpleAnimationの内部をいじくってフレーム制御が可能になります。

keepStoppedPlayablesConnected→false

にして対応、のはず。実際色々いじって動くようになりました。
(ただパフォーマンスに影響でるかもしれないので注意)

他に確か未定義のパラメータがあるから正常に動作しないって話だとおもうので、これを定義すればとりあえず動く形にはなります。
(あとパラメータがあったけど忘れたな・・・)

それでも動かない場合、CommandBufferで描画する・Animationを垂れ流し(LoopをOFFにする)・IKで制御、のいずれかになるかと思います。(特に3DのAnimationを利用する場合、フレーム制御と当たり判定のズレが発生する可能性があるので、IK制御にせざるを得ないんじゃないかと)

もうちょっと突っ込んだTips

Tween関数を自作

http://nakamura001.hatenablog.com/entry/20111117/1321539246

自作は可能。

GameObject使っていいのであれば位置の制御は可能なので、簡単にTweenオブジェクトを作ることが可能。

ただし動作はフレーム制御に依存することになるので注意。

シェイク処理

ダメージ時にシェイクする処理ね。

これはランダムの座標変更+一定の倍率で収束する、という処理を書けばOK。

上記と同じでフレーム制御になるのに注意。

動き全般でIKを導入する

IK=InverseKinematic

これならGameObjectに追従する形でキャラを制御でき、更に多関節キャラの制御も可能になるので、Animationを使わなくてもキャラの動きを制御できる。

FastIKやFinalIKが存在。(自分はFastIKを利用してる

GameObjectを除外する

ここまではまだやってないけど、Unityの機能を使わないのであれば、GameObject自体を削除することも可能。

元々GameObjectには自動で色々なことを行ってくれるように機能が仕込まれてるんだけど、これらを全部利用しないのであればGameObjectの利用を減らしてキャラクターの情報を生成・破棄だけを担当させる等は可能。

描画もCommandBuffersを利用すれば3D空間での描画も可能になるため、それほど困ることはなくなる。

ただ、削除するかはゲームの構造に依存する。(Raycastを使わない、Sceneで直接調整を行わない等であれば有用)

終わりに

すっげー駆け足で書いてみました。
意外と共有されない内容なので、知識を得るきっかけになれば幸いです。

参考資料

https://madewithunity.jp/stories/aka-to-blue/
アカとブルー(クラシックなルールで実装している例)

https://github.com/Unity-Technologies/SimpleAnimation
SimpleAnimation(アセット)

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

mercurial extensions/hooks for Unity

Unity開発どころか世の中全体的に「ふつーGitだよね」みたいな雰囲気だが、もちろんmercurialも使える。

extension

複数の platform の並行作業に便利だと思われる。

mergetools

Unityの一部のファイルはYamlの形式を装ってるが、実は不完全準拠なので、mercurial内蔵のmergeではファイルが壊れる可能性がある。具体的には、例えばGameObjectルートキーが複数回現れるが、Yamlの正規の仕様ではこれを認めてないためParseに失敗する。

なので、Unityご謹製のマージツールを使用するのが推奨されている。らしい。

hooks

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

バーチャル登壇用にスライド回転機構を作る

slide.gif

これは、 VTuber Tech #1 Advent Calendar 2019 の15日目の記事です。
昨日は @kaikiofkaiki さんによる バーチャルキャラクターの表情表現のためのデバイスとソフトウェア でした。

バーチャル登壇用にスライド回転機構を作る

今年もVTuber Tech Advent Calendarが開催するということで、去年同様ノリだけで枠を確保したわけですが、ネタが決まらなくてちょっと悩んでいました。

そんなところに、ありがたいことに、 大xR Tech Nagoya #8 さんにお声かけいただき、 バーチャル登壇 をさせていただけることになりました!

バーチャル登壇というのは、VR空間内でVRアバターがスピーチしている様子を撮影し、それをリアルタイムで会場に配信することです。

やっていることはVTuber Techと言えなくもないので、今回はバーチャル登壇をネタにしようと思います。

VRならではのことがしたい

登壇というと、プロジェクター等で大スクリーンにスライド資料を表示し、そのスライド資料をベースに発表するというイメージがあります。

それをそのままVR空間内でやってもいいのですが、なんとなく VRならでは のことがしたいなと思いました。

色々考えた結果、以下の発想に至りました。

スライド資料を立体にしたらおもしろいのでは?

どうやって資料を一覧表示するか考えた

色々なやり方があると思いますが、今回はVRChatのワールドを作ることにしました。

スライド資料をVRChat内で管理することになるわけですが、スライドの一覧をどうやって見るのか?という問題が浮上しました。

ぱっと思いついたのが、円盤 でした。

というわけで、以下のように 円形に並べてみました。

circle.gif

実際のスライド資料をこの記事でお見せするとネタバレになってしまうので、ダミーでボールを置いています。このボールが、スライド資料のページに相当します。

上空から見下ろすとこんな感じです。

image.png

ボールは20°間隔で並んでいますので、全部で18個分、設置できます。

どうやってスライドを進めるのか考えた

以下の図のような仕掛けを考えました。

右側のボタンを押すと、 次のページ に進み、左のボタンを押すと、 前のページ に戻ります。

ページが切り替わる際に、円盤全体が回転するようにアニメーションします。

slide.gif

それでは、詳しい実現方法を説明していきます。

AnimationIntAddでPageを増減できるようにした

実はVRChatのワールド制作ははじめてでよくわかってなかったのですが、VRChatでボタンを実装する場合、VRChat SDKの VRC_Trigger というコンポーネントを使うそうです。(STYLYでいうところの Trigger Enter のようなものだと思います。)

VRC_Trigger > Actions > Basic Events を眺めてみたところ、 AnimationIntAdd という項目が目に入りました。

どうやら、スイッチを押すたびに、 AnimatorParameters の数値を増やしていくことができるみたいです。

これで ページ番号を増やしたり減らしたりすることができるのでは?

と思い、 Animator > ParametersPage という変数を追加しました。

image.png

そして、ボタンに VRC_Trigger をアタッチし、 ActionsAnimationIntAdd に設定しました。

image.png

Variable には PageValue には 1 を入力しました。

これで、このボタンを押したときに、 Page という変数が 1 増えるようになりました。

Value-1 を入れれば、ページ番号を減らすこともできます。

ページの数だけアニメーションを用意した

ページ番号をボタンで増減できるようになったので、今度はそれをトリガーにして、アニメーションさせることを考えます。

「ページ番号をもとに円盤の回転角度を計算し、それにもとづいてアニメーションをさせる」ということをやりたかったのですが、C#スクリプトが書けないVRChatだとどうも難しそうだったので、

ページの数だけアニメーションとステートを用意することにしました

image.png

円盤の角度が

  • 0°から20°へ変化するアニメーション
  • 20°から40°へ変化するアニメーション
  • 40°から60°へ変化するアニメーション
  • ・・・

といった具合に、1つ1つアニメーションを作成しました。

ようするに力業ですw

(もう少し効率的な方法があれば、どなかた教えてください。)

TransitionsのConditionsにPageを設定した

これらの大量のアニメーションステート間をトランジション(矢印)でつなぎ、そのトランジションの条件に Page の大小比較を設定していきました。

image.png

これで、スライドを進めたり戻したりできるようになりました!

slide.gif

まだ開発中

ここまで偉そうに色々書きましたが、まだこのシステム開発中なんです。

完成したあかつきには 大xR Tech Nagoya #8 さんでお披露目したいと思います!

さいごに

日々色んなジャンルの記事を書いてますが、なんだかんだVRの記事書くのが一番楽しいです。

本記事作成にあたり、以下の記事を参考にさせていただきました。ありがとうございました。

これは、 VTuber Tech #1 Advent Calendar 2019 の15日目の記事でした。
明日は @kirimin さんの記事です!楽しみですね!

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

【Unity】 Oculus Quest のバッテリー情報を取得する

Oculus Quest でバッテリー残量を取得する方法

実装例

    float hmd = SystemInfo.batteryLevel;
    byte left = OVRInput.GetControllerBatteryPercentRemaining(OVRInput.Controller.LTouch);
    byte right = OVRInput.GetControllerBatteryPercentRemaining(OVRInput.Controller.RTouch);
    Debug.LogFormat("Battery: HMD = {0}%, Left Controller = {1}%, Right Controller = {2}%", 
        (int)(hmd * 100), left, right);

HMDのバッテリー残量

コントローラのバッテリー残量

  • OVRInput.GetControllerBatteryPercentRemaining で取得できる
    引数にコントローラ種別を渡せば、バッテリー残量がパーセンテージ数値(0~100)で返ってくる。
    ※ SystemInfo.batteryLevelのように0~1のfloat値ではないことに注意

参考

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