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

UdonSharp日記~道の動的生成3~

モチベーション維持のためにLGTMください。

やったこと

今回は道の動的生成をバージョンアップさせました。
具体的には下記の2点を行いました。
1.障害物を設置することで迷路のような作りにすること
2.レシピを作成し、それを入力として生成するつくりにすること

1.の意図
 あらかじめプレハブで道のバリエーションを持っておくことも考えたのですが、逐次生成している都合、道の形状のつじつまが合わせられないケースがあると考え、障害物を設置することで迷路を作る構成としました。
 おいおい障害物を取り除くような処理を組むかもしれませんので、このやり方がよいかなと。
2.の意図
 現在、道のインスタンス化にVRCInstantiateを使用していますが、この関数は非同期です。同期を考えた場合、すべてのユーザーで同じようにVRCInstantiateを実行する必要があります。
 つまり、迷路のレシピを同期させておけば間接的に同期できるはずです。

やりかた

今回はUdonSharpの使い方というよりも、ゲームプログラミングとか設計とかそういう感じですね。

今回の大切なところは、迷路のレシピをどのように作成するかという点です。
道の形状はソースコード中0~6の数字が割り当たった7パターンと、その回転4パターンです。
どの形、どれくらいの回転、座標 この三つの情報がスタックされていれば、
途中から入ってきたプレイヤーも迷路を再現できるようになります。
(今回は同期についての実装はありません)

なおレシピのフォーマットは次の通りです。
nrx,y,z
n:道の形状
r:90度の回転回数
x,y,z(リスポーン地点を0,0,0とした際のマス目)

    //どのフロアを作成するか選定する
    //入力はフロアを作成する座標 と 北0, 東1, 南2, 西3どちらに進んでいるか0~3
    //出力はフロアのレシピ
    //フロアレシピ定義
    //フロア形状n(0~6) y軸回転r(0~3) 座標
    //n定義           r定義
    //0 :┼          0 : 0度
    //1 :│          1 : 90
    //2 :├          2 : 180
    //3 :┌          3 : 270
    //4 :・(行止り)
    //5 :↑上り階段
    //6 :↓下り階段
    private string FM_choiceGenerateFloor(Vector3 createPos, int orient)
    {
        int[] n_kouho;
        if ((int)createPos.y == 0)
        {
            //高さが0の時はフルセット
            n_kouho = new int[] { 0, 1, 2, 3, 4, 5, 6 };
        }else if ((int)createPos.y == (int)FM_FLOOR_HIGHT)
        {
            //高さが2階のとき
            n_kouho = new int[] { 0, 1, 2, 3, 4, 6 };//上りを除く
        }
        else
        {
            n_kouho = new int[] { 0, 1, 2, 3, 4, 5};//下りを除く
        }
        //候補からランダムに選択する
        int n = n_kouho[(int)Random.Range(0, n_kouho.Length)];

        int[] r_kouho;
        //道の形状によって許される回転回数が決まっている。(入り口には障害物を置いてはいけない)
        switch (n)
        {
            //01456は回転しない
            case 0:
            case 1:
            case 4:
            case 5:
            case 6:
                r_kouho = new int[] {0};
                break;
            case 2:
                //2は0,90,180の三択
                r_kouho = new int[] {0, 1, 2};
                break;
            case 3:
                //3は0,90の二択
                r_kouho = new int[] {0, 1};
                break;
            default:
                r_kouho = new int[] {0};
                break;
        }
        //回転もランダムに選択し、そこに、プレイヤーがどちらの方角から入ってきたかを加味する
        int r = (r_kouho[(int)Random.Range(0, r_kouho.Length)] + orient) %4;
        //座標そのままだとレシピが長いため、マス単位にする
        int x = (int)(createPos.x / (FM_FLOOR_SIZE_HALF * 2));
        int z = (int)(createPos.z / (FM_FLOOR_SIZE_HALF * 2));
        int y = (int)(createPos.y /  FM_FLOOR_HIGHT );
        return string.Format("{0}{1}{2},{3},{4}",n.ToString() , r.ToString() ,x,y,z);
    }

課題

まだこの方法で同期が実現できるかわかっていません。
近いうちに同期のテストを実施します。

同期テスト結果

同じワールドにいるならば同期できそうです。
後から入ってきた人もレシピをもとに同期をとるはずが、取れる場合と取れない場合がありました。

原因を探さねば

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

Google Cloud Text-To-Speechを利用してUnityでキャラクターをフルボイスに!

タイトルの通りです.
UnityでもGoogle CloudのAPIを利用することで(簡単に)音声合成を行うことができます.

余談

Google CloudのText-To-Speech APIを利用することで誰でも手軽に音声合成を行うことができます.
しかし,これを利用した記事は現状あまり公開されていません.

特にC#(.NET)環境での動作については,Google Cloudのドキュメンテーションページでも
図1.png
と非常に残念な感じです.

今回これを触る機会があり,動くようにするために結構苦労しました.
実装の際特に詰まった部分について解説しながら動くサンプルを示したいと思います.

準備

Unityプロジェクトに導入するためにあらかじめ行う作業があります.

Google Cloud Platformの利用登録

まず,Google Cloudに開発者申請を行い,APIリクエストを送る際の認証情報を取得する必要があります.
Google Cloud Platformのページにアクセスし,利用者登録を行います.
住所,名前,電話番号,クレジットカード番号を入力する欄がありますが,個人開発者であれば基本的に無料で利用できます.

料金ページに利用料の記載があります.
Text-To-Speechの場合,クラウドに送信した文字数に応じて料金が変動します.
通常の音声合成とWaveNet音声(ちょっとリアルな声)で値段が変わり,以下のように無料枠と有料の場合の値段が定められています.
image.png

利用者登録を行った後は分かりやすい名前を付けてプロジェクトを作成します.
image.png
その後プロジェクトでCloud Text-To-Speech APIを有効にします.
左上のHamburgerタブからメニューを開き,「APIとサービス」から「ライブラリ」を選びます.
image.png
そこでCloud Text-To-Speechを選択し,APIを有効化します
image.png

次に実際に利用するAPIキーを作成します.
左上のHamburgerタブからメニューを開き,「IAMの管理」から「サービスアカウント」を選択し,サービスアカウントの作成を行います.
image.png
「作成」を選択し,次に出る組織の選択はスルーして大丈夫です.
次の「キーの作成」は必須です.
Json形式でキーを作成し,保存します.(このキーは厳重に管理してください)
image.png
これでGoogle Could Platform側での作業は終わりです.

Unityにライブラリをインポートする

現在Google Cloud Text-To-SpeechをUnityで利用できるようなライブラリは存在しません.
なので自分で利用するライブラリ(dll)をインポートする必要があります.

NuGetからライブラリをインポートする

Google Cloud APIはNuGetで.Net用ライブラリが公開されており,それを利用します.
UnityにNuGetライブラリをインポートするための便利なツールに「NuGetForUnity」というものがあるので,これを利用しUnityに導入していきます.
これを利用することで依存するライブラリをまとめてインポートできるので便利です.

検索窓に「Cloud TextToSpeech」と入力することで目的のAPIが見つかるのでこれをインストールします.
image.png

これでAssetフォルダに利用するdllがインポートできましたが,このままではunload broken assemblyというエラーが出ます.
これはdll間で依存するライブラリを見つけられていないためで,これを回避するためにはいろいろ方法がありますが,今回は一番単純な「すべてのdllを同じディレクトリに置く」という手段を取ります.結構めんどくさいです.

インポートを行った後NuGetForUnityはもう不要なので,Assetのディレクトリから該当するフォルダを削除します.(dllの入っているPackagesフォルダは消さない!
そしてpackages以下に存在するdllを全てpackagesの先頭フォルダに移し,纏めます.
これでエラーは出なくなったはずです.

ランタイムで利用するライブラリをインポートする

NuGetからライブラリをインポートすることでエディタ上ではエラーが出なくなりますが,このままでは実行時にDllNotFoundException: grpc_csharp_extEntryPointNotFoundException: grpcsharp_native_callback_dispatcher_initといったエラーが出てしまいます.

ランタイム(実行時)で利用するライブラリはhttps://github.com/jsmouret/grpc-unity-package からお借りします.
https://github.com/jsmouret/grpc-unity-package/releases から最新のgrpc-unity-package.zipをダウンロードし,Plugins/Grpc.Core/runtimes/win(利用環境によって変えてください)/x64/grpc_csharp_ex.dllをコピーし,先ほどNuGetからインポートしたフォルダに貼り付けます.

これで実行時のエラーもなくなります.

スクリプトの用意

TextToSpeechSampleというC#スクリプトを新たに作成し,以下のスクリプトを貼り付けます.

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Cloud.TextToSpeech.V1;
using Grpc.Auth;
using Grpc.Core;
using UnityEngine;
using UnityEngine.UI;
using WWUtils.Audio;
using Debug = UnityEngine.Debug;

public class TextToSpeechSample : MonoBehaviour
{
    [SerializeField] private InputField inputField;
    [SerializeField] private Button button;
    [SerializeField] private AudioSource audioSource;
    [SerializeField] private string credential;

    private const string GcpUrl = "https://www.googleapis.com/auth/cloud-platform";
    private const string ChannelTarget = "texttospeech.googleapis.com:443";

    private TextToSpeechClient _client;
    private AudioConfig _audioConfig;
    private VoiceSelectionParams _voiceSelectionParams;

    private ChannelCredentials _credentials;

    private SynchronizationContext _context;

    private Stopwatch _stopwatch = new Stopwatch();

    private void Start()
    {
        // ボタンを押したときのイベントを追加
        button.onClick.AddListener(() =>
        {
            var str = inputField.text;
            if (string.IsNullOrEmpty(str)) return;
            inputField.text = "";

            CreateRequest(str);
            Debug.Log($"Send Request: {str}");
        });

        // 認証情報をResourceから読み込む
        var credentialStr = Resources.Load<TextAsset>(credential).text;
        var googleCredential = GoogleCredential.FromJson(credentialStr);
        _credentials = googleCredential.CreateScoped(GcpUrl).ToChannelCredentials();

        var channel = new Channel(ChannelTarget, _credentials);
        _client = new TextToSpeechClientImpl(new TextToSpeech.TextToSpeechClient(channel), new TextToSpeechSettings());

        // オプションを記述
        _audioConfig = new AudioConfig()
        {
            AudioEncoding = AudioEncoding.Linear16,
            SampleRateHertz = 44100
        };

        // 声のパラメータを指定
        // https://cloud.google.com/text-to-speech/docs/voices?hl=jaに記載されているものから選択できます
        _voiceSelectionParams = new VoiceSelectionParams()
        {
            SsmlGender = SsmlVoiceGender.Female,
            LanguageCode = "ja-JP"
        };

        _context = SynchronizationContext.Current;
    }

    /// <summary>
    /// リクエストを送信する
    /// </summary>
    /// <param name="text">音声合成を行う対象の文</param>
    public void CreateRequest(string text)
    {
        var request = new SynthesizeSpeechRequest
        {
            Input = new SynthesisInput {Text = text},
            AudioConfig = _audioConfig,
            Voice = _voiceSelectionParams
        };

        _stopwatch.Restart();

        // リクエストを非同期で送信し,返ってきた後に再生するメソッドに投げる
        Task.Run(async () => { SetAudioClip(await _client.SynthesizeSpeechAsync(request)); });
    }

    /// <summary>
    /// Google CloudからのレスポンスをAudioClipに書き出し,再生する
    /// </summary>
    /// <param name="response">Google Cloudからのレスポンス</param>
    private void SetAudioClip(SynthesizeSpeechResponse response)
    {
        var bytes = response.AudioContent.ToByteArray();

        // byte[]をAudioClipで利用できる形に変換する
        var wav = new WAV(bytes);
        Debug.Log("Get Response: Elapsed time " + _stopwatch.ElapsedMilliseconds + "ms.\nData Length: " +
                  (wav.SampleCount * (1f / wav.Frequency) * 1000f).ToString("F0") + "ms.");
        _context.Post(_ =>
        {
            // AudioSourceに新しいAudioClipを貼り付ける
            audioSource.clip = AudioClip.Create("TextToSpeech", wav.SampleCount, 1, wav.Frequency, false);
            audioSource.clip.SetData(wav.LeftChannel, 0);

            // AudioClipを再生
            audioSource.Play();
        }, null);
    }
}

このスクリプトでは以下の処理を記述しています

  • ButtonとInputFieldのイベントを追加
  • 認証キー(Json)を読みこむ
  • Google Cloudとの接続を行うクライアントの作成
  • リクエストを送信する
  • リクエストの返信からAudioClipを作成し,再生する

Google Cloudからのレスポンスはbyte[]で送られてくるので,それをAudioClipで利用できるようfloat[]に変換する必要があります.
そのためにここで示されているWAVクラスを導入します.

新しいスクリプトを作成し,記述されているスクリプトを新しいC#スクリプトに貼り付けます.

Sceneの用意

これで完成です.
以下のようにInputFieldとButtonを持つ新しいシーンを作成します.
image.png
空のGameObjectを作成し,AudioSourceと先ほど作成したTextToSpeechSampleをアタッチします.

先のGoogle Cloud Platformの利用登録で取得したJsonの認証情報をResourcesのフォルダ内に置きます.
TextToSpeechSampleにInputFieldとButton,AudioSourceを登録し,"Credential"にResource以下のJsonファイルのディレクトリを指定します.(例:Assets/Resources/Credentials/credential.jsonに格納した場合,Credentials/credentialと記述)

これで完成です.

実行する

実行し,InputFieldに発言させたい文を記述し,"Send"ボタンを押すことでリクエストが送られ,少し待つと合成された音声が再生されます.
細かくは調べていませんが,リクエスト最初の一回は少し時間がかかり,二回目以降は「こんにちは」といった短い文であれば0.3秒ほどで再生されます.

今回のスクリプトでは応答にかかった時間,文を読み上げるのにかかる再生のログ表示も併せて行われます.
image.png

終わりに

Google Cloud Text-To-Speechを利用することで,簡単に(記事を書いてて思いましたが,結構大変でした)Unityで音声合成を行うことができます.

【Unity】自分の声をテキスト化する方法【Google Cloud Speech Recognition 】など,Google Cloud Speech Recognitionを利用した音声認識に関する記事は結構存在するので,少し調べればUnity上で音声認識もできます.
同じGoogle Cloudを利用することで今回冒頭のGoogle Cloud Platform利用者登録の大変な部分を次回以降はなくせるので比較的楽に実装できるかと思います.

これを利用したチャットボットなど作成するのも面白そうです.

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

ARKitでヴァーチャル背景つくってみた

今日この頃

新型コロナウイルスの影響で、リモート会議やリモート飲み
などが盛ん(?)に行われていると思います。

Zoomなどのサービスを使うと、カメラを使って顔出しをする際に
周りの背景などを写したくない人の為に、人物部分だけを抜き出して
好きな背景を設定できる「ヴァーチャル背景」機能があります。
(いろいろな会社さんがヴァーチャル背景用の画像をツイートしていたりします)

スクリーンショット.png

使ってみると結構綺麗に抜けます。
おそらく機械学習でデータを作成して利用しているのでしょう。

今回の騒動の影響で「リモートで〇〇する」というのは需要が伸びそうな気がしていて、
配信系のアプリを作る際も「ヴァーチャル背景」機能は必須となるかもしれません。

ですが、調べてみると案外簡単に利用できる技術やサービスがありません
(機会学習のデータは作成に手間が掛かっているので簡単には流れてくることはないでしょう)

なので

ARKit3から実装されたPeople Occlusionの機能を使って
似たような機能をつくってみました。

ARKitの機能で人物とそれ以外で分けられた画像が取得できます。
その画像がまさにMask画像なのでそのままガッツリ利用して、楽に実装していきます。

スクリーンショット 2020-04-27 15.21.25.png

環境

Unity2019.3.7f1
iPad Pro 11 インチ (第2世代)

実装

・前提
UnityでARKitを利用する為にAR Foundationを使います。
インストールや設定などは、他にも詳しく解説しているサイトが多くあるので省きます。

Versionは4.0.0を使用しました。
スクリーンショット 2020-04-27 15.29.08.png

・シーン作成
AR Session OriginAR Session配置し
AROcclusionManager.csのスクリプトを追加します。

AR Cameraに追加されている
ARCameraBackground.csのスクリプトは
使用しないので非アクティブにします。

表示したい背景用にImage(BackgroundImage)
RawImage(MaskRawImage)を配置しておきます
スクリーンショット 2020-04-27 15.42.35.png
スクリーンショット 2020-04-27 16.19.25.png
スクリーンショット 2020-04-27 16.19.40.png

・画像の取得と割り当て
カメラ映像とマスク用の画像を取得してそれぞれを割り当てます。

マスク処理はShaderで行うので適応したMaterial
RawImage(MaskRawImage)に設定します
スクリーンショット 2020-04-27 16.21.24.png

Manager.cs
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class Manager : MonoBehaviour
{
    public ARCameraManager cameraManager;
    public AROcclusionManager occlusionManager;

    private Texture2D camaraImage;

    public RawImage maskRawImage;
    public Material maskMaterial;

    void OnEnable()
    {
        cameraManager.frameReceived += OnCameraFrameReceived;
    }

    void OnDisable()
    {
        cameraManager.frameReceived -= OnCameraFrameReceived;
    }

    unsafe void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
    {
        XRCameraImage image;
        if (!cameraManager.TryGetLatestImage(out image))
        {
            return;
        }

        var format = TextureFormat.BGRA32;

        if (camaraImage == null || camaraImage.width != image.width || camaraImage.height != image.height)
        {
            camaraImage = new Texture2D(image.width, image.height, format, false);
        }

        var conversionParams = new XRCameraImageConversionParams(image, format, CameraImageTransformation.MirrorY);

        var rawTextureData = camaraImage.GetRawTextureData<byte>();
        try
        {
            image.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length);
        }
        finally
        {
            image.Dispose();
        }

        camaraImage.Apply();

        Texture2D humanStencil = occlusionManager.humanStencilTexture;

        maskRawImage.texture = camaraImage;
        maskMaterial.SetTexture("_MaskTex", humanStencil);

    }
}
SpriteWithMask
Shader "Custom/SpriteWithMask" {
    Properties {
        _MainTex ("Base", 2D) = "white" {}
        _MaskTex ("Mask", 2D) = "white" {}
        _Color ("Color", Color) = (0.5, 0.5, 0.5, 0.5)
    }
    SubShader {

    Cull Off

        Pass {
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

sampler2D _MainTex;
sampler2D _MaskTex;
float4 _Color;

struct v2f {
    float4 pos : SV_POSITION;
    float2 uv1 : TEXCOORD0;
    float2 uv2 : TEXCOORD1;
};

float4 _MainTex_ST;
float4 _MaskTex_ST;

v2f vert (appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos (v.vertex);
    o.uv1 = TRANSFORM_TEX (v.texcoord, _MainTex);
    o.uv2 = TRANSFORM_TEX (v.texcoord, _MaskTex);

    o.uv2.x = 1.0 - o.uv2.x;

    return o;
}

half4 frag (v2f i) : COLOR
{
    half4 base = tex2D (_MainTex, i.uv1);
    half4 mask = tex2D (_MaskTex, i.uv2);
    base.w = mask.x * mask.x * mask.x;

    return base;
}
            ENDCG
        }
    } 
    FallBack Off
}

結果

ezgif.com-video-to-gif.gif

現状と今後

なかなか良い精度のものが作れましたが、ARKit限定かつ、対応機種が
まだ少ない状態なので、もう少し普及率が高まらないと採用できないのかな
と思います。

こういう時ARKitとARCoreが並走してくれないのが、もどかしいですね・・・
(どうしようもないことですが・・・)

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

ARKitでバーチャル背景つくってみた

今日この頃

新型コロナウイルスの影響で、リモート会議やリモート飲み
などが盛ん(?)に行われていると思います。

Zoomなどのサービスを使うと、カメラを使って顔出しをする際に
周りの背景などを写したくない人の為に、人物部分だけを抜き出して
好きな背景を設定できる「バーチャル背景」機能があります。
(いろいろな会社さんがバーチャル背景用の画像をツイートしていたりします)

スクリーンショット.png

使ってみると結構綺麗に抜けます。
おそらく機械学習でデータを作成して利用しているのでしょう。

今回の騒動の影響で「リモートで〇〇する」というのは需要が伸びそうな気がしていて、
配信系のアプリを作る際も「バーチャル背景」機能は必須となるかもしれません。

ですが、調べてみると案外簡単に利用できる技術やサービスがありません
(機会学習のデータは作成に手間が掛かっているので簡単には流れてくることはないでしょう)

なので

ARKit3から実装されたPeople Occlusionの機能を使って
似たような機能をつくってみました。

ARKitの機能で人物とそれ以外で分けられた画像が取得できます。
その画像がまさにMask画像なのでそのままガッツリ利用して、楽に実装していきます。

スクリーンショット 2020-04-27 15.21.25.png

環境

Unity2019.3.7f1
iPad Pro 11 インチ (第2世代)

実装

・前提
UnityでARKitを利用する為にAR Foundationを使います。
インストールや設定などは、他にも詳しく解説しているサイトが多くあるので省きます。

Versionは4.0.0を使用しました。
スクリーンショット 2020-04-27 15.29.08.png

・シーン作成
AR Session OriginAR Session配置し
AROcclusionManager.csのスクリプトを追加します。

AR Cameraに追加されている
ARCameraBackground.csのスクリプトは
使用しないので非アクティブにします。

表示したい背景用にImage(BackgroundImage)
RawImage(MaskRawImage)を配置しておきます
スクリーンショット 2020-04-27 15.42.35.png
スクリーンショット 2020-04-27 16.19.25.png
スクリーンショット 2020-04-27 16.19.40.png

・画像の取得と割り当て
カメラ映像とマスク用の画像を取得してそれぞれを割り当てます。

マスク処理はShaderで行うので適応したMaterial
RawImage(MaskRawImage)に設定します
スクリーンショット 2020-04-27 16.21.24.png

Manager.cs
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public class Manager : MonoBehaviour
{
    public ARCameraManager cameraManager;
    public AROcclusionManager occlusionManager;

    private Texture2D camaraImage;

    public RawImage maskRawImage;
    public Material maskMaterial;

    void OnEnable()
    {
        cameraManager.frameReceived += OnCameraFrameReceived;
    }

    void OnDisable()
    {
        cameraManager.frameReceived -= OnCameraFrameReceived;
    }

    unsafe void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
    {
        XRCameraImage image;
        if (!cameraManager.TryGetLatestImage(out image))
        {
            return;
        }

        var format = TextureFormat.BGRA32;

        if (camaraImage == null || camaraImage.width != image.width || camaraImage.height != image.height)
        {
            camaraImage = new Texture2D(image.width, image.height, format, false);
        }

        var conversionParams = new XRCameraImageConversionParams(image, format, CameraImageTransformation.MirrorY);

        var rawTextureData = camaraImage.GetRawTextureData<byte>();
        try
        {
            image.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length);
        }
        finally
        {
            image.Dispose();
        }

        camaraImage.Apply();

        Texture2D humanStencil = occlusionManager.humanStencilTexture;

        maskRawImage.texture = camaraImage;
        maskMaterial.SetTexture("_MaskTex", humanStencil);

    }
}
SpriteWithMask
Shader "Custom/SpriteWithMask" {
    Properties {
        _MainTex ("Base", 2D) = "white" {}
        _MaskTex ("Mask", 2D) = "white" {}
        _Color ("Color", Color) = (0.5, 0.5, 0.5, 0.5)
    }
    SubShader {

    Cull Off

        Pass {
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

sampler2D _MainTex;
sampler2D _MaskTex;
float4 _Color;

struct v2f {
    float4 pos : SV_POSITION;
    float2 uv1 : TEXCOORD0;
    float2 uv2 : TEXCOORD1;
};

float4 _MainTex_ST;
float4 _MaskTex_ST;

v2f vert (appdata_base v)
{
    v2f o;
    o.pos = UnityObjectToClipPos (v.vertex);
    o.uv1 = TRANSFORM_TEX (v.texcoord, _MainTex);
    o.uv2 = TRANSFORM_TEX (v.texcoord, _MaskTex);

    o.uv2.x = 1.0 - o.uv2.x;

    return o;
}

half4 frag (v2f i) : COLOR
{
    half4 base = tex2D (_MainTex, i.uv1);
    half4 mask = tex2D (_MaskTex, i.uv2);
    base.w = mask.x * mask.x * mask.x;

    return base;
}
            ENDCG
        }
    } 
    FallBack Off
}

結果

ezgif.com-video-to-gif.gif

現状と今後

なかなか良い精度のものが作れましたが、ARKit限定かつ、対応機種が
まだ少ない状態なので、もう少し普及率が高まらないと採用できないのかな
と思います。

こういう時ARKitとARCoreが並走してくれないのが、もどかしいですね・・・
(どうしようもないことですが・・・)

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

【Unity】Terrainをエクスポートして別プロジェクトで使う

スクリーンショット 2020-01-02 0.21.30.png


TL;DR

  • テレインを別プロジェクトに共有する際にハマったのでメモ
  • テレインのあるプロジェクトを丸ごとインポートする

環境

  • Unity2019.3.6(エクスポート元、インポートイ先共通)
  • Windows10

課題

  • terrainをエクスポートして、別のプロジェクトで利用したいと考えました
  • というわけで、とりあえずprefab化してインポート先に投げつけてみました(prefab化などがわからない人は別途おググりください)
  • 結果、以下のようになってしまいました

スクリーンショット 2020-01-02 0.21.30.png
形は再現できたのですが草木、といいますか表面のテクスチャが反映されません。(激怒)

というわけで、まずはterrainのテクスチャを出力する方法を探ったのですがいい方法が見つからず、沼でした

やったこと

プロジェクトをシーンごとエクスポートする

  • ツールバーのAssetsからExport Packageでプロジェクトをエクスポートします
    • 関係ないファイルは除外しましょう
    • シーンを含めるのをお忘れなく!
  • 先ほどエクスポートしてきたシーンを開き、共有先のプロジェクトで、テクスチャ付きでテレインがあるのを確認します
    • ここでも反映されない場合は、共有元のプロジェクトからエクスポートする際に必要なものが抜け落ちているかもしれません
  • 別のシーンで反映させたい場合は、prefab化したterrainを改めてヒエラルキーに投げます

↓結果です
スクリーンショット 2020-01-02 0.21.30.png

期待通り、表面のテクスチャ付きで反映することができました!(祝)

参考

なし

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

UdonSharp日記~メニューを自作する~

やったこと

今回はキーボードのESCを押した際にオリジナルのメニューを表示する機能を作成しました。
今手元にviveコンがないのでデスクトップモードのみを対象としています。
(発火させるボタン登録を変更すればVRモードでも実行可能)

参考文献

uGUIからUdonSharp関数を呼び出す方法について
ハツェさんのUdon#入門③

やり方

Canvasを作成します。
メニューを出すのに直接は関係ないですが、同時にuGUIからmanagerのUdonSharp関数呼び出しと、値の取得を試したかったため、Button, Slider, Textを追加しています。

無題.png

無題2.png

実装の方針は
1.ESCキーが押されたら
 2.メニューがアクティブならば非アクティブにして消す
 3.メニューが非アクティブならば ユーザーの座標近くにメニューを移動して アクティブにする。

という考えです。

    private void UM_Update()
    {
        if (Input.GetKeyDown(KeyCode.Escape)) 
        {
            if (UM_userMenue.activeSelf)
            {
                //メニューがアクティブならば、非アクティブ化
                UM_userMenue.SetActive(false);
            }
            else
            {
                //非アクティブならば、プレイヤーの前に移動しアクティブ化

                //座標設定
                Vector3 pos = GEN_localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
                Quaternion rot = GEN_localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).rotation;
                //オイラー角に変換後、メニューが垂直になるようにx軸を0にする。
                Vector3 rotEuler = rot.eulerAngles;
                rotEuler.x = 0;
                //オイラー角をクォータニオンに戻し、プレイヤー座標とずらす距離を計算する
                rot = Quaternion.Euler(rotEuler);
                Vector3 bias = rot * new Vector3(0f, 0f, 0.5f);//0.5mだけ前に出す
                //座標設定
                UM_userMenue.transform.position = pos + bias;

                //角度設定
                UM_userMenue.transform.rotation = rot;

                //アクティブ化
                UM_userMenue.SetActive(true);
            }
        }
        //プレイヤーとの距離が遠い場合は非アクティブ化
        float seqDist = Vector3.SqrMagnitude(GEN_localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position - UM_userMenue.transform.position);
        if(seqDist > 4.0f)
        {
            UM_userMenue.SetActive(false);
        }
    }

クォータニオンはよくわかりませんが、Vector3に掛け合わせると、クォータニオンの軸に従って回転してくれること。
直接操作できなくてもオイラー角に直せばなんとなく加工できることがわかりました。

メインとなるUodate関数は次の通り

    void Update()
    {
        GEN_Update(); //前回の記事参照
        UM_Update();  //今回追加したもの
        FM_Update();  //前回の記事参照
    }

uBUIのボタンからUdonSharp関数を呼び出す方法については参考文献にそのまま乗っているので、今回は記載しません。

他のオブジェクトから引数渡しでUdonSharpを呼び出すことはできませんが、uGUIのTextにデータを書き込んでから呼び出せば、実質データを渡しての外部からの実行ができそうですね。
ボタンを押すと、Textへ渡すデータを書き込む関数と、動かしたい関数を呼び出せば、うまくいきそう・・・

でももし二つが非同期で同時に走ったらそれもそれで面倒な予感はあります。

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

対象の明るさをそのままに,周囲のみを暗くするエフェクトを実装する

この記事では,この画像のようにあるオブジェクト以外の画面を暗くすることのできるエフェクトの実装方法について紹介します.
図1.png

あるオブジェクトだけを暗くしたり,あるオブジェクト以外を明るくしたり,その逆もできるので一風変わった強調表現として使えるかと思います.

この記事はUnity2018.4.22f1とUnity2018.4.0f1のWindowsビルドで動作検証しています.バージョンやプラットフォームによってうまく動かないことがあるかもしれません.

サンプルとしてUnityちゃん(© Unity Technologies Japan/UCL)を利用させていただきました.

実装

今回のエフェクトを実装するために,

  • 明るさを保持したいオブジェクトにその旨を記述するためのシェーダ
  • 実際にエフェクトを適用させるポストエフェクトシェーダ
  • エフェクトをカメラに適用するためのC#スクリプト

の3つのコードを記述する必要があります.
これらの実装を順に,軽く説明していきます

明るさを保持したいオブジェクトにその旨を記述するためのシェーダ

このシェーダでは「その旨」を記述するわけですが,書き込むために「ステンシルバッファ」を利用します.
細かい話は補足として最後に載せました.

以下に一般的なUnlitシェーダ(光源の影響を受けないシェーダ)にステンシルバッファの記述したサンプルを載せますが,Stencil{...}部分を他のシェーダに移植することで問題なく実行できるはずです.

Shader "Unlit/TextureWriteStencil"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry-1"}

        Pass
        {
            Stencil
            {
                Ref 1
                Comp Always
                Pass Replace
            }

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

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

実際にエフェクトを適用させるポストエフェクトシェーダ

ポストエフェクトに記述するシェーダも通常のオブジェクトを塗るためのシェーダと同様の書き方をします.
詳しい話は省略しますが,画面に表示される画像が入力として得られるので,その入力にステンシルバッファの書き込みがあった場合に処理を実行するようにします.
ここでもステンシルバッファを利用して条件分岐を行います.

以下に対象のステンシル以外の色を暗くするシェーダスクリプトを載せます.
Durationの値でカメラ画像と塗りつぶす色をグラデーションさせています.

Shader "Hidden/DimEffect"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
        _FogColor ("Fog Color", Color) = (0, 0, 0, 1)
        _Duration ("Duration", Range(0, 1)) = 0
    }

    SubShader
    {
        Cull Off ZWrite Off ZTest Always

        Pass
        { 
            Stencil
            {
                Ref 1
                Comp NotEqual
            }

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;

            float4 _FogColor;
            float _Duration;
            float _StencilMask;

            struct v2f {
                half4 pos : POSITION;
                half2 uv : TEXCOORD0;
            };

            float4 _MainTex_ST;

            v2f vert(appdata_base v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            half4 frag(v2f i) : COLOR
            {
                float4 color = tex2D(_MainTex, i.uv);
                float3 lerpColor = lerp(color, _FogColor, _Duration).rgb;
                return float4(lerpColor, color.w);
            }

            ENDCG
        }
    }
}

エフェクトをカメラに適用するためのC#スクリプト

オブジェクトに対するシェーダは適用するマテリアルにアタッチすればよいですが,ポストプロセスシェーダを適用するためには,そのシェーダがアタッチされたマテリアルをカメラに渡してあげる作業が必要になります.

Post Processing Stack v2はその辺の処理を自動化してくれる便利なやつですが,「ステンシルバッファの値がクリアされた後の画面情報しか得られない」という重大な問題があり,これをそのまま利用することはできません.この事実を探すのにとても苦労しました...

以下のスクリプトを作成し,利用するカメラにアタッチします.

using UnityEngine;
using UnityEngine.Rendering;

[ExecuteAlways]
[RequireComponent(typeof(UnityEngine.Camera))]
public class DimEffect : MonoBehaviour
{
    [SerializeField] private bool state;
    [SerializeField] private Shader dimEffectShader;
    [SerializeField] private Color fogColor;
    [Range(0f, 1f)][SerializeField] private float duration;

    private Material _material;
    private static readonly int _FogColor = Shader.PropertyToID("_FogColor");
    private static readonly int _Duration = Shader.PropertyToID("_Duration");

    private UnityEngine.Camera _camera;
    private CommandBuffer _buffer;
    private int _tempTextureIdentifier;

    private bool _currentState = false;

    public void SetState(bool value)
    {
        if (_currentState == value) return;            
        _currentState = value;

        if (value)
        {
            Init();
            _camera.AddCommandBuffer(CameraEvent.AfterForwardAlpha, _buffer);
        }
        else
        {
            _camera.RemoveCommandBuffer(CameraEvent.AfterForwardAlpha, _buffer);
        }
    }

    public void OnValidate()
    {
        SetState(state);

        if (_material != null)
        {
            _material.SetColor(_FogColor, fogColor);
            _material.SetFloat(_Duration, duration);
        }
    }

    private void Init()
    {
        if (_camera == null)
        {
            _camera = this.GetComponent<UnityEngine.Camera>();
        }

        if (_material == null)
        {
            _material = new Material(dimEffectShader) {hideFlags = HideFlags.DontSave};
        }
        _material.SetColor(_FogColor, fogColor);
        _material.SetFloat(_Duration, duration);

        if(_tempTextureIdentifier == 0)  _tempTextureIdentifier = Shader.PropertyToID("_PostEffect");

        if (_buffer == null)
        {
            _buffer = new CommandBuffer {name = "DimEffect"};

            _buffer.GetTemporaryRT(_tempTextureIdentifier, -1, -1, 0);
            _buffer.Blit(BuiltinRenderTextureType.CameraTarget, _tempTextureIdentifier);
            _buffer.Blit(_tempTextureIdentifier, BuiltinRenderTextureType.CameraTarget, _material);
            _buffer.ReleaseTemporaryRT(_tempTextureIdentifier);
        }
    }
}

実行結果

このようなサンプルステージを用意しました.
before_fade.png
カメラに上のC#スクリプトをアタッチし,図のようにパラメータを設定します.
image.png
Stateがエフェクトをかけるかどうかで,Dim Effect Shaderの部分にポストエフェクト用シェーダを貼り付けます.

手前の一つの物体にのみ先ほど作成したステンシルを書きこむシェーダーのついたマテリアルをアタッチし,エフェクトを適用すると以下のような描画が得られます.

after_fade.png

このように,特定の物体のみ色をそのままに,残りの部分を暗くするエフェクトを実装することができました.
少し違ったオブジェクトの強調表現として使えそうです.

おわりに

今回はステンシルバッファとポストエフェクトを組み合わせることで対象の物体(以外)にエフェクトをかける方法について説明しました.
周囲の明るさを暗くする方法は今回述べたとおりですが,結構汎用性のありそうな技術なのでいろいろとできることがありそうです.(対象だけ黒く塗りつぶす,発光させる等...?)

補足

ステンシルバッファとは

ステンシルバッファは,シェーダ要素の一つです.レンダリング時に描画するかどうかを判断します.
簡単に言うとif文のようなもので,対象の物体を描画するときに,それ以前に描画された背景オブジェクトのステンシル値と比較して描画するかどうかを決定します.

Shader "Hidden/Sample"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Pass
        {
            Stencil
            {
                Ref 1
                Comp Always
            }
     ・・・

のように記述します.
SurfaceシェーダではSubShader内に,FragmentシェーダではPass内に記述します.

各シェーダの意味が分からない方は,
シェーダ内にSurfaceシェーダは #define surface と記述されているもの,Fragmentシェーダは #define fragment と書かれているものがそれだと思ってください.

Stencilブロック内の記述

今描画しようとしている背景の物体がStencilブロック内の条件を満たす場合に描画処理を実行します.
Stencilブロック内には以下のような要素を記述することができます.詳しい内容は公式リファレンスマニュアルを確認してください.

  • Ref (Number)

(Number)に数字を記載し,以下のブロックでこの数字を参照することで比較を行います.
デフォルト値は0です.

  • ReadMask (Number)

(Number)に数字を記載し,読み込みを行う場合の比較対象とします.

  • WriteMask

(Number)に数字を記載し,書き込みを行う場合の比較対象とします.

  • Comp (CompFunction)

(CompFunction)に条件を書くことで,その条件を基に比較を行います
詳しい内容は省きますが,条件には

Greater(Ref < Mask)
Less(Ref > Mask)
Equal(Ref == Mask)
NotEqual(Ref != Mask)

等があります.

  • Pass (StencilFunction)

Compの条件に合致した場合のステンシル値の操作方法を定義します.

  • Fail (StencilFunction)

Compの条件に合致しなかった場合のステンシル値の操作方法を定義します.
条件には

Keep : 書き換えを行わない
Zero : 0を代入する
Replace : Refの値を記入する

等があります.

例えば

Stencil
{
    Ref 2
    Comp Equal
    Pass Zero
}

のように記述することで背景のブロックを参照し,2が記述されていた時に0を代入します.

ステンシルに記述するときの落とし穴

ステンシルバッファに値を書き込めばそれ以降に参照されるシェーダにその値を伝えることが可能になりますが,数点気にしなければいけない場所があります.

  • レンダリング順序に気を付ける

当然ですが,ステンシルバッファに値を入れても他のシェーダに値を上書きされてしまうとその値は参照することができません.
そのために,描画順について意識する必要があります.
特に今回はポストプロセスに使用するためにレンダリングの最終段階まで残るようにしなければいけません.
そのために,SubShaderのTagプロパティとして

SubShader
{
    Tags { "RenderType"="Transparent" "Queue"="Transparent+1"}
    ....

等記述する必要があります.
Transparentは透明な物体の描画順位なので,これに+1することで確実に値を残すことができます.
詳しい描画順位についての話はドキュメントを参照してください.

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

新作講座 Unity3Dゲーム Standard講座を発売したって話 

 

この度、 Unity3Dゲーム Standard講座を発売しましたー。

こちらは3年ぶりの新作講座なので かなり久しぶりですね。

名前のとおり 3Dゲームに特化した内容で
Unityの基礎をマスターできる内容となっています。 

動画で約6時間のボリュームです。 

更には 作ったゲームを アプリとしてリリースするまでの過程も全部
解説しています。 
【Androidでの解説だけです。iosはありませんので】

今年に入って Unityを教えることなども多くなってきたので 

更に 第3弾の講座も予定しています。 

本当は、 勉強会などもやる予定だったんですけど。 
コロナの影響で 会場とかカラオケ店なども 使えなくなりましたからね。

なので、勉強会はしばらくあおずけです。 

今は皆さん 引きこもって ゲームとかしている人も多いと思いますけど 

これを機に、 ゲーム作りなどをしてみてはどうでしょうか? 

PS

講座はNoteにて 販売しています。 
https://note.com/zazizuzezo22334/n/n90340339a00c?magazine_key=m3f4ee59f0352

只今 緊急事態セールということで 70%OFFで 購入できます。 

あと、 C#プログラミングマスター講座も 好評発売中です。 
こちらも70%OFFにしています。 

この2つをセットで 購入するお客さんが多いですね。

やはりC#が分からないと Unityも理解できないので 
C#の勉強もちゃんとしたほうがいいかと思います。

NOTEでも ちょいちょいUnityネタを投稿しています。

よかったら、フォローしてください。

bandicam 2020-03-22 05-55-31-149.png

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

【Unity:C#】Attributeの定義をまとめる

VSで自動フォーマット適用した時にAttributeの定義が改行されて行が増えるのが地味に嫌だった

sample.before.cs
[SerializeField]
[Range(0, 1)]
private int sample;

カンマ区切りでいけたんだね

sample.after.cs
[SerializeField, Range(0, 1)]
private int sample;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOS CatalinaのUnity2018.2.20f1でオーディオファイルをインポートするとフリーズする

各種バージョン

ソフトウェア バージョン
macOS Catalina 10.15.4
Unity 2018.2.20f1

概要

既存 Unity プロジェクトでアセットの依存関係を整理していると、「Inconsistency Detected」というエラーが出た。

Inconsistency Detected

An inconsistency has been detected in your project's AssetDatabase. This should never happen.
If possible, please report a bug, attaching your project and describing any asset-related actions you took leading up to this point.
Continuing may lead to strange behaviour, loss of data, and crashes. Rebuilding the AssetDatabase via the 'Reimport All' command is recommended.

どうやら何かのミスで AssetDatabase に不整合が生じてしまったらしい。
エラーダイアログによると「Reimport All」で解決するらしいので、早速 Asset -> Reimport All を実行した。

しかし、何時間待っても Importing Assets が完了しないので、プログレスバーを見に行くと .mp3 のオーディオファイルのインポート処理で停止してしまっていた。該当のオーディオファイルを一旦削除して再度 Reimport All しても今度は別のオーディオファイルでフリーズしてしまう...。(mp3, wav, ogg でも同様の問題が発生)

これは想像よりも面倒な問題なのでは無いかと思ったので、調査した結果をまとめておく。

原因

Installing Unity on macOS Catalina - Unity Forum に答えがあった。

The Wav file import issue with 2017.4 LTS, we have identified the issue as being an issue with an FMOD Tool being 32bit which is not supported in MacOS 10.15. We will release a fix for 2017.4 LTS but we will not be releasing an update for 2018.1/2018.2 as these are unsupported releases. The issue doesn't manifest in 2018.3+ and we will update the documentations etc.. to represent the support matrix below.

古いバージョンの Unity ではオーディオファイルのインポートで32ビットのFMODツールを利用していたが、これが Catalina ではサポート対象外になってしまった事が原因な模様だった。よって、Catalina に対応している Unity バージョンへアップグレードする事で本問題は解決可能である。

解決策

Catalina は下記バージョンの Unity しか対応していないので、これを使えば解決。

  • 2017.4 LTS
  • 2018.3 / LTS
  • 2019.1+

弊社では 2018.2.20f1 → 2018.4.22f1 にアップグレードして対応した。
(2019.1+ へのアップグレードはプロジェクトが正常に動作しなくなったので一時断念)

なお、アップグレードによるコードの変更等の影響は(今のところ)特に無かった。

最後に

元々 macOS 10.14 を利用して Unity 開発をしており、途中で OS のアップグレードを行った。
この時点ではすでにオーディオファイルはインポート済みなので本事象は発生してなく、Asset の再インポートを行った時に初めて問題が浮き彫りになったため、Catalina が原因だとは全く想像がつかなかった。

そのため、プロジェクトの git clone やり直しや、UnityEditor の再インストール等で試行錯誤をしてしまったため、問題の解決に多くの時間が掛かってしまった。

もしかしたら自分と同じように苦しんでいる人がいると思うので、この記事が誰かの助けになれば幸いである。

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

macOS CatalinaのUnityでオーディオファイルをインポートするとフリーズする

各種バージョン

ソフトウェア バージョン
macOS Catalina 10.15.4
Unity 2018.2.20f1

概要

既存 Unity プロジェクトでアセットの依存関係を整理していると、「Inconsistency Detected」というエラーが出た。

Inconsistency Detected

An inconsistency has been detected in your project's AssetDatabase. This should never happen.
If possible, please report a bug, attaching your project and describing any asset-related actions you took leading up to this point.
Continuing may lead to strange behaviour, loss of data, and crashes. Rebuilding the AssetDatabase via the 'Reimport All' command is recommended.

どうやら何かのミスで AssetDatabase に不整合が生じてしまったらしい。
エラーダイアログによると「Reimport All」で解決するらしいので、早速 Asset -> Reimport All を実行した。

しかし、何時間待っても Importing Assets が完了しないので、プログレスバーを見に行くと .mp3 のオーディオファイルのインポート処理で停止してしまっていた。該当のオーディオファイルを一旦削除して再度 Reimport All しても今度は別のオーディオファイルでフリーズしてしまう...。(mp3, wav, ogg でも同様の問題が発生)

これは想像よりも面倒な問題なのでは無いかと思ったので、調査した結果をまとめておく。

原因

Installing Unity on macOS Catalina - Unity Forum に答えがあった。

The Wav file import issue with 2017.4 LTS, we have identified the issue as being an issue with an FMOD Tool being 32bit which is not supported in MacOS 10.15. We will release a fix for 2017.4 LTS but we will not be releasing an update for 2018.1/2018.2 as these are unsupported releases. The issue doesn't manifest in 2018.3+ and we will update the documentations etc.. to represent the support matrix below.

古いバージョンの Unity ではオーディオファイルのインポートで32ビットのFMODツール(サウンドエフェクトエンジン)を利用していたが、これが Catalina ではサポート対象外になってしまった事が原因な模様だった。よって、Catalina に対応している Unity バージョンへアップグレードする事で本問題は解決可能である。

解決策

Catalina は下記バージョンの Unity しか対応していないので、これを使えば解決。

  • 2017.4 LTS
  • 2018.3 / LTS
  • 2019.1+

弊社では 2018.2.20f1 → 2018.4.22f1 にアップグレードして対応した。
(2019.1+ へのアップグレードはプロジェクトが正常に動作しなくなったので一時断念)

なお、アップグレードによるコードの変更等の影響は(今のところ)特に無かった。

最後に

元々 macOS 10.14 を利用して Unity 開発をしており、途中で OS のアップグレードを行った。
この時点ではすでにオーディオファイルはインポート済みなので本事象は発生してなく、Asset の再インポートを行った時に初めて問題が浮き彫りになったため、Catalina が原因だとは全く想像がつかなかった。

そのため、プロジェクトの git clone やり直しや、UnityEditor の再インストール等で試行錯誤をしてしまったため、問題の解決に多くの時間が掛かってしまった。

もしかしたら自分と同じように苦しんでいる人がいると思うので、この記事が誰かの助けになれば幸いである。

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

ゲームの銃撃音実装バリエーションその2 武器の状態に合わせて音を自動的に変える

ゲーム開発におけるサウンド演出実装の難しい点は、「ゲームは常に状況が同的に変化する」ということです。動画制作と異なり、ゲームではプレイヤーの入力によって音が鳴る状況はダイナミックに変化します。
3Dのゲームでは単にサウンド素材をステレオで鳴らすだけではなく、ゲーム内の状況に沿った音の聞こえ方・方角・音の種類を場面に合わせていく必要があります。
今回は三人称シューティングゲームにおけるサウンド実装を想定し、プレイヤーが銃のモードを自由に変えながら、ゲーム中のカメラの視点も変わるような場合の音の実装について解説します。

前提となる知識

この記事は
「ゲームの銃撃音バリエーションを増やしつつコード量を減らす」https://qiita.com/Takaaki_Ichijo/items/fced234c2aca996f9fd5

と、「ゲームの状況に合わせてサウンドエフェクトのかかり具合を変える」
https://qiita.com/Takaaki_Ichijo/items/957dd09112b9939ff083
の姉妹編です。

また、本記事は統合型サウンドミドルウェア「CRI ADX2」及び付属のサウンドオーサリングツールAtom Craftの触り方をある程度理解している方向けの解説です。
初めてADX2を使う、という方は各エンジンへの導入の記事を先に参照ください。

Unity: Unityのサウンド機能をADX2で強化する
https://qiita.com/Takaaki_Ichijo/items/16e6501fc07f5b3b3377

UE4: ADX2 for UE4の導入で、一歩上のサウンド表現を(導入編)
https://qiita.com/SigRem/items/4250925f6d66a4fd287a

「属性付与」演出で銃の発射音に別の音を追加する

戦闘の要素があるゲームでは、ゲームシステムに深みを持たせるために「属性」の設定をつけることが多くあります。
例えば炎は氷に強いが土に弱い、雷は水に弱い...など、武器や防具に属性をつけ、そのバランスでゲーム内の戦略に強弱をつけるものです。

シューティングンゲームのサウンド実装において、「銃の発射音」にこうした属性の概念で音が変わるような実装を要求されることを考えてみてください。
素直に考えるならば、「雷モードのときの音」「氷モードのときの音」のように、サウンド素材をバラバラに用意して、そのつど鳴らすファイルを変える実装が思いつきます。
ただ、銃の種類が数十個、属性の概念も数十個、となると用意すべき音が膨大なバリエーションを持つときはどうすればいいでしょう。

2つ目の手段としては、「銃の発射音」と「属性を表す音」を別々に用意しておき、同時に鳴らすことです。これなら大量のバリエーションをデータとして持たずに済みそうです。
しかしながら、再生するべきファイルが2つになり、同時に再生する音の数をカウントして制御していた場合は、その判定が複雑になります。
加えて、属性の種類に応じてボリュームを調整したい...などとなった場合はどうでしょうか。

そこで今回は、CRI ADX2を使って「属性がつく銃の音」の設計と管理について紹介します。

元素材の用意

銃の発射音は、前々回の記事と同じフリー効果音サイトの「効果音ラボ」から取得します。

「属性」にあたるサウンド素材は、Tsugi株式会社の「DSP Anime」を使って生成します。

dspアニメ.png

これらのサウンド素材を用いて、サンプルとして今回は「通常の銃の発射音」と「雷属性のついた銃の発射音」を切り替えできる音声データを作成します。

「セレクタラベル」を使ったキューの設定

銃の音に「属性」の概念を導入するには、ADX2の「セレクタラベル」機能を使います。

まずは、ベースとなる「銃の音」を用意します。この設定は前々回の記事ゲームの銃撃音バリエーションを増やしつつコード量を減らすと同様です。

handganbase.png

このキューを直接再生せずに、「属性」を設定したキューの中にトラックとして埋め込んで再生する仕組みを取ります。
「キューリンク」という機能を用いて、キューの中にキューを設定します。

キューリンク.png

次に、この銃の音と同時に再生する「属性」を表す音のトラックを作ります。

雷属性.png
キューリンクトラックの下にある「Track_Electricity」が雷の「バリバリ」という音の素材です。
このキューをプレビュー再生すると、当然両方が同時に再生されます。一定の設定を行ったときのみ、下のトラックが再生されるようにしましょう。

プロジェクトツリーで「攻撃の属性」を表すセレクタラベルを作成する

プロジェクトツリーで、ゲームにおける「武器の状態」を示すラベルデータを作成します。
「セレクタフォルダ」の下に新しいセレクタとして「Enchant」を作ります。

セレクタフォルダ.png

Enchantセレクタフォルダの中に、無属性攻撃を示す「None」と、雷属性攻撃を示す「Thunder」のセレクタラベルを作成します。

セレクタラベルをキューのトラックに指定する

トラックにプロジェクトツリーで作成したセレクタラベルを指定します。

セレクタラベルの指定.png

銃の発射音は常になってほしいので何もラベルは付けず、2番目のトラックの雷の音に「Thunder」ラベルを指定します。
これでキューの設定は完了です。メニューの「表示 -> セッションウィンドウ」を選択し、ラベルの有無で音が変わるかどうかをチェックします。

セッションウィンドウでセレクタの動作を確認.png

セレクタラベルをキューの再生時に指定

プログラムからセレクタラベルを指定して再生する
ゲーム側で「ラベル」を指定して再生します。

Unityの場合

CriAtomSourceクラスメンバのCriAtomExPlayerクラスにああるSetSelectorLabelメソッドで切り替えます。
Enumを用意しておくと明瞭です。

https://game.criware.jp/manual/unity_plugin/jpn/contents/classCriAtomExPlayer.html#a70e17b68b0e8cb38589a966e71395d64

PlayGunShot.cs
public class PlayGunShot: MonoBehaviour 
{
    public CriAtomSource atomSource;
    public CriAtomExAcb acb; //再生するキューが入ったキューシート
    public enum GunEnchant {Fire, Ice, Thunder};

    public PlayHandGunShot(GunEnchant  gunEnchant)
    {
         atomSource.player.SetCue(acb, "Handgun_Shot");

         if(gunEnchant == GunEnchant.Thunder)
         {
             atomSource.player.SetSelectorLabel ("Enchant","Thunder");
         }
         atomSource.Play();
    }
}

UE4の場合

ブループリントAPI「Atom Component」の「Set Selector Label」ノードを使います。

https://game.criware.jp/manual/ue4_plugin/contents/criware_ue4_api_AtomComponent_SetSelectorLabel.html

ほかの武器効果音へセレクタラベルの転用

一度作成したセレクタラベルは再利用できますので、例えば剣を振る音や防具で攻撃を防御する音で「属性」を表したい場合も使えます。

銃を構えている位置で聞こえる方向性を変える

2000年代以降のTPS(三人称シューティング)では、自キャラが中央より左右にどちらか寄っていて、肩越しにレティクルが見えているゲームが多くなりました。バイオ4やギアーズオブウォー、Fallout 3以降のTPS視点などです。
こういった視点の場合、敵などの見たいものと自キャラがかぶってしまったときのために、左右の視点切り替えが実装されています。効果音の一部も、この左右の寄りにそって鳴る位置が変わるとより自然です。
そこで、プレイヤーの位置(ゲームのカメラ視点)によって、音の聞こえる方向が変わる実装を考えてみます。

キューごとのラベル指定ではなく、全体設定を切り替える

さきほどの「属性」で説明したセレクタラベルは、各キューに毎回「今回鳴らすときはこのラベル」という指定を行っていました。
ですが、ゲーム中のカメラ位置が変わって音の聞こえる方向が変わる、というのはすべての武器から発せられる音が影響されるということです。
こういったサウンド全体に影響を及ぼすゲーム内の変化には、セレクタではなく「ゲーム変数」機能を用いると効果的です。

ゲーム変数は、ADX2側に0~1の変数を用意しておき、特定のキューは必ずその数値を参照して音の鳴り方を変更する機能です。
ゲーム内の状況が変わった時にその変数の値を変更すれば、関連するキューはその値をもとに鳴り方が変化します。

0から1の数値が変化するにしたがって、再生中の音を滑らかに変化させたい....という場合は「AISAC」機能を使うとよいのですが、今回の例のパターンは発射音が鳴った直後に視点を変更したとき、音もシームレスに変化してしまっては不自然です。

ゲーム変数を使って、「視点が右の場合」と「左の場合」の設定をあらかじめ作っておき、一気に切り替える方針で考えていきます。

ゲーム変数とキューの用意

まずはプロジェクトツリーで「ゲーム変数」フォルダを開き、「PlayerSCereenPositon」変数を新規作成します。

ゲーム変数の用意.png

また、先ほど作成したHandgun_Shotキューを再び入れ子にします。今回は同じキューを2トラックに並べます。

パンの設定.png

そのうえで、それぞれのトラックの「パン[5.1]」設定を別の喪に変えます。一つ目は右に振り、二つ目は左に振ります。

キューにゲーム変数を指定する

キューとゲーム変数を関連させるために、キューのインスペクタで「スイッチ」を選び、先ほど作成したゲーム変数「PlayerScreenPosition」をスイッチ変数欄に指定します。

スイッチの設定.png

これでこのキューはゲーム変数に応じて再生されるトラックが変わるようになりました。
先ほどと同様「セッションウィンドウ」を開き、「ゲーム変数」ウィンドウを開いてスライダーを動かしながらプレビュー再生してみましょう。

スイッチ変数の確認.png

スライダーが0のときと1のときで再生される音の聞こえる方向が変わることが確認できます。

スクリプトからの「ゲーム変数」変更方法は前回記事と同様です・

効果音のバリエーションを豊かに、ただし手間は増やさずに

冒頭で紹介したように、たくさんの音のバリエーションがあると再生時にどの音を再生するべきかの管理が大変になります。

ADX2とその機能セレクタラベル、ゲーム変数を活用することで、スクリプトからの呼び出しを複雑化せずに、ゲーム内の状況を反映する音を組むことができます。

今回は説明しませんでしたが、ピッチ変更やフィルタの適用なども組み合わせれば、より広い表現が可能です。ぜひチャレンジしてみてください。

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

OculusQuestでゲームをするためのジョイスティックを作る

はじめに

OculusQuestでゲームをするためにジョイスティックが欲しくなったので作ってみました。

XR Interaction Toolkitを使っているのでOculusQuest以外でも動きそうですが未検証です。

開発環境

Unity 2019.3.10f1
XR Interaction Toolkit 0.9.4
Oculus XR Plugin 1.3.3

作成方法

構成

image.png
image.png

土台(Base)

・ジョイスティック自体をつかんで操作するために、XR Grab Interactableスクリプトをつけます。
・XR Grab Interactableで必要なのでRigidBodyをつけます。
・つかんでいないときに動かないように、RigidBodyのIsKinematic=trueにしておきます。

スティックの下側のボール(Ball1)

・このオブジェクトを回転させることで、スティックを動かします。
・XR Stick Interactableスクリプト(スティックを操作するための自作スクリプト、後で説明します)で必要なのでRigidBodyをつけます。
・スティックを操作するためのスクリプトはこのオブジェクトにつけてしまうと認識されないため(上位の階層にXRBaseInteractable派生のスクリプトがあるとダメみたいです)、別の階層につけます。

スティック(Cylinder)

・スティックをつかむ判定に必要なのでColliderをつけます。

制御(handle)

・XR Stick Interactableスクリプト(スティックを操作するための自作スクリプト)をつけます。
・XR Stick InteractableスクリプトのCollidersには操作対象のスティックのColliderを、MovingRigidBodyには動かすボールのオブジェクトを、OnStickChangeには動きを通知する関数を登録します。
・SetTextは動きの確認表示用、GetInputActionは動きをゲームに伝えるためにつけているスクリプトです。

XRStickInteractable.cs
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Interaction.Toolkit;

public class XRStickInteractable : XRBaseInteractable
{
    [Serializable]
    public class StickChangeEvent : UnityEvent<float, float> {}

    public Rigidbody MovingRigidbody;
    public StickChangeEvent OnStickChange;

    private XRBaseInteractor m_GrabbingInteractor;
    private Quaternion m_StartRotation;
    private Vector3 m_CacheTarget;
    private Quaternion m_CacheRotation;


    private Quaternion CheckStickValue(Vector3 target, Vector3 center)
    {
        Quaternion rotation = m_CacheRotation;

        if (m_CacheTarget != target)
        {
            m_CacheTarget = target;

            Vector3 relativePos = target - center;
            if (relativePos.y < 0)
            {
                relativePos.y = 0;
            }
            rotation = Quaternion.LookRotation(relativePos);
            m_CacheRotation = rotation;

            if (OnStickChange != null)
            {
                try
                {
                    float axis_h = Vector3.Angle(transform.right, relativePos);
                    float axis_v = Vector3.Angle(transform.forward, relativePos);
                    float horizontal = (90 - axis_h) / 90;
                    float vertical = (90 - axis_v) / 90;
                    OnStickChange.Invoke(horizontal, vertical);
                }
                catch
                {
                    Debug.LogError("A delegate failed to execute for OnStickChange in XRStickInteractable");
                }
            }
        }
        return rotation;
    }

    // Start is called before the first frame update
    void Start()
    {
        if (MovingRigidbody == null)
        {
            MovingRigidbody = GetComponentInChildren<Rigidbody>();
        }
        if (MovingRigidbody != null)
        {
            m_StartRotation = MovingRigidbody.rotation;
        }
    }

    public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
    {
        if (isSelected)
        {
            if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Fixed)
            {
                if (MovingRigidbody != null)
                {
                    Quaternion rotation = CheckStickValue(m_GrabbingInteractor.transform.position, MovingRigidbody.transform.position);
                    MovingRigidbody.MoveRotation(rotation);
                }
            }
        }
    }

    protected override void OnSelectEnter(XRBaseInteractor interactor)
    {
        base.OnSelectEnter(interactor);

        m_GrabbingInteractor = interactor;
    }

    protected override void OnSelectExit(XRBaseInteractor interactor)
    {
        base.OnSelectExit(interactor);

        if (MovingRigidbody != null)
        {
            MovingRigidbody.MoveRotation(m_StartRotation);
        }
    }
}

GetInputAction.csは操作対象のゲームにより書き換える必要があります。
水平方向の動きがhorizontalにより-1~1の範囲で、垂直方向の動きがverticalにより-1~1の範囲で通知されてくるので、それをゲームにあった動きに変換します。GetInputは操作対象のゲームから呼ばれる処理です。

GetInputAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetInputAction : MonoBehaviour
{
    private float m_CacheHorizontal = 0;
    private float m_CacheVertical = 0;

    public void GetStickValue(float horizontal, float vertical)
    {
        m_CacheHorizontal = horizontal;
        m_CacheVertical = vertical;
    }
    public float[] GetInput()
    {
        var action = new float[2];
        if (m_CacheHorizontal > 0.5f)
        {
            action[1] = 1f; //move right
        }
        else if (m_CacheHorizontal < -0.5f)
        {
            action[1] = 2f; //move left
        }
        if (m_CacheVertical > 0.5f)
        {
            action[0] = 1f; //move up
        }
        if (m_CacheVertical < -0.5f)
        {
            action[0] = 2f; //move down
        }
        return action;
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】iOSでテクスチャがガビガビに汚くなる問題の対処方法

Unityのデフォルト設定だと、Androidにて正常に表示されるテクスチャがiOSでは画質が劣化する場合があります。
左:Android 右:iPhone
and00.jpg ip01.jpg

これを防ぐにはテクスチャの圧縮方式を変更する必要があります。
なお一番手っ取り早いのは、圧縮を無効にする事です(容量は増えるでしょうが…)

該当のテクスチャを選択しInspectorの下の方にある Default で Override for iOS をチェックします。
Unity04.png
Unity05.png

その後、Formatを変更します。無圧縮なら RGBA 16 bit か RGBA 32 bit に変更すればOKです。
アイコンのような色数の少ない画像なら16bitでも大丈夫ですが、グラデーションするような画像(スカイボックス等)は32bitにした方が良いでしょう。

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

【Unity】Build Target AndroidPlayer is not supported エラーを防止する方法

Unityを起動した時たまに以下のエラーが出て来て、終了するかWindowsにSwitchTargetしろとか無茶を言いやがります。

Build Target AndroidPlayer is not supported. Switch to WindowsStandaloneSupport or exit Unity?
Unity00.png

これの防ぎ方ですが、起動時にLearnタブを一回クリックしておくと良いようです。
理由は分かりませんが…
Unity01.png

この方法はフォーラムで紹介されてました
https://forum.unity.com/threads/build-target-android-is-not-supported-switch-to-windows-or-exit-unity.625336/

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

Unity Package Manager 対応にさせる

Assets/ に実行時に必要のないエディター拡張用のファイルも追加しなければならない、そんな状況も Unity Package Manager に対応することで解決出来るようになりました。嬉しいですね。

※ ローカルパッケージとして読み込んだ状態

と思って色々と調べていたのですが、今時点では遊びや好奇心の範疇を超えなかったので、真面目に運用しようと思ったら Unity 2020 飛んで Unity 2021 あたりかな、という印象でした。

エディター拡張の管理はシンボリックリンクを使った方法が現実的ですが、Unity Package Manager に興味があれば。

--

※ Unity Package Manager で推奨(?)されている、Editor フォルダーに対する Runtime フォルダーという存在は、Assembly Definition 作りが大変になる従来の Scripts 内に Editor フォルダーを置く、という手法より良い感じ。

もくじ

はじめに

Unity エディターで汎用的に使用するエディター拡張であっても、通常は Assets/ 以下の Editor/ フォルダーにファイルをコピーすることになります。

ゲームに必要なコンポーネントは動作確認済みのバージョンのコピーであって欲しいので問題ないですが、エディター拡張は常に最新の機能を持った物を使いたいですよね。

しかし、エディター拡張関連のファイルもコピーなので、特定のプロジェクトに存在するファイルを更新しても他のプロジェクトには更新が適用されません。

※ 最新のものを使う以前に、結構便利に使ってたアレ、いつのどのプロジェクトにファイルがあるんだっけ?? ともなりがち。

で、

や、

を使ってどうにかしていた訳です。

シンボリックリンクとフォルダージャンクションの違い

Windows のショートカットは 「.lnk という拡張子を持ったエクスプローラー向けのファイルフォーマット」 という厄介な存在でしたが、少し前から Windows でも Linux と同じようなシンボリックリンクとフォルダージャンクションという機能が使えるようになっています。

この機能を使ってファイルの実体は一か所、そのファイルを参照するプロジェクトは沢山ある、を実現するわけですが、両者には少しだけ違いがあります。

  • シンボリックリンク

    • シンボリックリンクは SourceTree 等の Git クライアントではフォルダーとして認識されないので、エディター拡張の共有には便利。※インストール時の設定でシンボリック云々を設定していない場合
    • ただし、ファイルシステムレベルでフォルダーとして認識されるわけではないので、Asset Store Tools でシンボリックリンク以下のファイルをアップしようとするとエラーが出る、等の問題も。
  • フォルダージャンクション

    • 対してフォルダージャンクションはより低層で機能しているようで、どのソフトウェアからも通常のフォルダーと区別なく認識され、Git クライアントや Asset Store Tools でも運用可能。

※ シンボリックリンクは Windows の機能、フォルダージャンクションはファイルシステム(NTFS)の機能、だったハズ。なのでジャンクションは FAT32 等でフォーマットされたディスクでは使えない。

Unity Package Manager 対応の利点

エディター拡張を Unity Package Manager(以下UPM)に対応させる利点は、

  • UPM の UI を使って気軽に追加・削除ができる。

  • 関連ファイルを Assets/ にコピーしたり、シンボリックリンクを作ったりしなくても良くなるので、ゲームそのものには不要なファイルが Assets/ 以下から一掃できる。

  • Packages/ 以下のファイルはコピーではなく参照なので、色々なプロジェクトに複製が散らばっているような事態を防げる。

といった所。

今時点では UPM の UI から独自パッケージの検索・バージョン管理等は出来ず、配布はかろうじて、という状態。検索が出来ないのが残念。

UPM に対応させるかどうか

で、今時点で UPM 運用に耐えるのは、以下のような Unity の基本機能にのみ依存した、単純・単機能なエディター拡張ぐらいかと。

Unity 標準ではない機能から呼び出されず、呼び出すこともしない物すね。

Assembly Definition(Assembly-CSharp.dll)の問題

UPM への対応では Assembly Definition(.asmdef)の使用は不可避なので、Unity 標準機能ではない機能に向けて作成したエディター拡張は UPM への対応が難しい。

UPM については元々 VRM 用のエディター拡張を対応させてみよう、と調べてたんですが、

  • VRM の .asmdef を追加でダウンロードして導入する。
  • VRM.Samples がある場合はエラーが出るので、VRM.Samples の .asmdef も導入する。

が、パッケージ受け入れ側のプロジェクトで必要に。

VRM 書き出しの為だけのプロジェクトであれば VRM 関連の .asmdef を導入するだけでおっけですが、VRM を使ったアプリを作るプロジェクトでも .asmdef を導入した場合、結果的にプロジェクトの全ての機能に .asmdef を用意する必要が出てくる。

.asmdef は全か無かというポリシーなので、ちょっと UPM 試してみよう、では厳しい。

--

Assembly Definition(.asmdef)の何が問題になるかについては、以下の記事が非常に参考になります。

エディター拡張をどうこうする程度なら熟読する必要は無いけども、ざっと目を通しておいて損は無いです。

.asmdef 対応の副産物

Assembly Definition を使う副産物で System.Reflection に頼らずに Unity のプライベートな機能にアクセスできるようにしたりも可能。

参考:

InternalsVisibleTo になっている .dll 名一覧:

Scene Timeline が Reflection でどうにかするエディター拡張だから、この情報は福音だった。(.dll 名が微妙になるけども)

その他

その他にも問題(?)があって、

  • UPM の UI が独自パッケージの更新や再ダウンロード、バージョン切り替えには対応していない。検索も出来ない。

  • パッケージをダウンロードする機能はあれど、Packages/manifest.json に URL とダウンロード済みか否かのステータス(正確には lock)も一緒に保存されている状態なので、バージョン管理ソフト経由で manifest.json を共有した場合、大丈夫??

等々。

そもそも、UPM が流行っていない現状で 「(実は)UPM に対応した独自のパッケージが必要なんすよ」 というのはトラブルの匂いしかしないので、ビルドに必要なファイルは従来通り、Assets/ 以下で管理を行い、その存在が自明になっている方が安心ですね。

UPM 対応手順

UPM 対応パッケージの作成方法は非常に簡単。

fuqunaga さんがマニュアル未公開の部分も含め、非常に詳しく紹介してくれています。

--

最低限の対応であれば、package.json.asmdef のふたつのファイルをフォルダーに配置してやるだけ。

package.json を作成

まずはパッケージのルートフォルダーに package.json という名前のテキストファイルを作成。

最低限記載する必要がある情報は以下の通り。

{
    "name": "com.sator-imaging.pose-editor",
    "displayName": "Pose Editor",
    "version": "1.0.0",
    "unity": "2017.1",
    "description": "Editor extension to pose a character.\n\nCopyright (c) Sator Imaging, all rights reserved."
}

※ ライセンス情報などを記載するべきファイルは UPM に仕様として存在するが、読んでほしい重要な情報は "description" に記載した方が良さそう。(ライセンス情報を記載したファイルが有ろうと無かろうと、docs.unity3d.com にリンクされ、当然ページは見つからない @ Unity 2018 LTS)
※ パッケージの管理名("name")は全て小文字(マニュアルにもちらっと記載アリ)そのくらい大丈夫だろうという感じだけど、vs code で注意が出ているところは直しておかないと Unity でエラーが出る。
※ 対応する Unity のバージョン("unity")は年号でないと受け付けないので、2017.1 が最も若いバージョン。
※ パッケージ自体のバージョン("version")はセマンティックバージョニングでないとエラーで弾かれる。

参考:

--

package.json さえあれば、UPM からパッケージとして読み込みが可能になる。

もう一つの必要なファイル、.asmdef はパッケージとして Unity に読み込んだ後に Unity 上で作成していく。

作成したローカルパッケージの読み込み方法は、こちら。

Assembly Definition(.asmdef)を追加

.asmdef は Unity エディターの Project ビューから、Create > Assembly Definition で作成する事が可能。

UPM の公式のマニュアルによれば、Runtime と Editor でアセンブリは分けるような事が書いてあるが、特段分けなければいけないという訳ではない。もの凄い大作のエディター拡張でもなければ、ルートに一つ置いておけば良いと思う。

また、エディター拡張を UPM パッケージにする場合に限れば、Platforms を Editor のみに設定するよりは、Define ConstraintsUNITY_EDITOR を設定しておく方が良い。

これはエディター拡張といいつつ、マニピュレーターを描画する等の関係で MonoBehaviour を継承したものが含まれることもある為。

Define ConstraintsRequired Preprocessor Symbols とかの方が分かり易くて良いのでは…? 拘束を定義するとは…。

--

UPM パッケージとしては、ここまでやれば最低限の仕様を満たしたものに。

後は UPM パッケージのフォルダー内に各種ファイルを追加すれば、Unity のプロジェクトビューの Packages 以下に追加したファイルが読み込まれ、使用可能な状態になる。

※ Assembly Definition でエラーが出た場合の対処方法は、@namazuchin さんの記事が非常に参考になります。

その他の UPM パッケージ用ファイル

UPM には package.json.asmdef 以外にも、パッケージに含めるべきファイルが以下のマニュアルに記載されているが、

  • README.md
  • CHANGELOG.md
  • LICENSE.md
  • Documentation~ ※ 末尾にチルダがあると Unity にインポートされない

等は追加したとしても UPM の UI は期待した通りには動かないので、UPM が独自パッケージの管理に完全に対応するまで、読んでほしい情報は package.json"description" に羅列しておくのが良さそう。

その他、UPM パッケージについての詳細は以下の公式マニュアルで確認できる。

抜粋: UPM パッケージのファイル構成

<root>
  ├── package.json
  ├── README.md
  ├── CHANGELOG.md
  ├── LICENSE.md
  ├── Editor
  │   ├── Unity.[YourPackageName].Editor.asmdef
  │   └── EditorExample.cs
  ├── Runtime
  │   ├── Unity.[YourPackageName].asmdef
  │   └── RuntimeExample.cs
  ├── Tests
  │   ├── Editor
  │   │   ├── Unity.[YourPackageName].Editor.Tests.asmdef
  │   │   └── EditorExampleTest.cs
  │   └── Runtime
  │        ├── Unity.[YourPackageName].Tests.asmdef
  │        └── RuntimeExampleTest.cs
  └── Documentation~
       └── [YourPackageName].md

サンプルシーンの扱い

UPM のパッケージにはどんなファイルでも追加する事が出来るが、.unity ファイルは Packages/ 以下から直接開く事が出来ない。

ただ、参照は可能なので、マルチシーンの子供としては追加が可能。アセットストアに UPM 対応として出品するときには、その旨記載しておくと良さそうです。

※ Unity 2019 だと直接 .unity ファイルを開けてしまうが、それはそれで問題がある気も…?

パッケージが見つからないとき

package.json を移動するなどして見つからなくなった場合、以下のエラーが出ます。

※ 「Continue」を押せば、とりあえず起動は出来る。

Windows なら UPM の UI から見つからないパッケージを消せばおっけですが、macOS だと UPM の UI が空っぽになるので、手動で Packages/manifest.json を編集する必要がある。

※ macOS だと UPM が空っぽに。

macOS は結構不遇な扱い受けてるんですね… ライセンス認証しても UI 明るいままだし。

おわりに

UPM 対応に必須な Assembly Definition はあまり流行っていないようなので、UPM の前にまずはそこから、という感じ。

今時点で UPM の為に使用できるオープンなレジストリもあるようだけど、Unity 的にアセットストアの前にそれらに対応するのか? というのもあると思うので、ごく一部の人が UPM でなんかやってる… という状態がしばらくは続きそうな感じすね。

自前でレジストリを建てる方法もあるみたいですが、プロジェクト全体を巻き込むほどの利便性があるのか&メンテナンスのコスト、社外に協力会社がいたら? 等々を考えると…

※ 将来的に公式のレジストリが用意されたとしても、プライベート公開が出来ず全世界公開オンリーだとしたら使いどころも限定的に… 難しいですね。

--

以上です。お疲れさまでした。

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