20210314のUnityに関する記事は9件です。

ROS講座123 UnityでROSと通信する

環境

この記事は以下の環境で動いています。

項目
CPU Core i5-8250U
Ubuntu 18.04
ROS Melodic
Gazebo 9.0.0
python 2.7.17

Unityの動作環境は以下です。

項目
ホストマシン Windows10
Unity 2019.4

インストールについてはROS講座02 インストールを参照してください。
またこの記事のプログラムはgithubにアップロードされています。ROS講座11 gitリポジトリを参照してください。

概要

これまでQtやブラウザでUIを作ってきましたが、今回はスマホで操作することを目標にしてUnityで作ってみます。
Unityはゲーム作成の統合開発環境で、ゲームで必要な3DCGや物理演算などの処理がを簡単に扱うことが出来ます。UnityはWindows/Mac/CentOSをサポートしていますが、今回はWindows上で開発します。
Unity本体だけでも基本的なゲームを作成することが出来ますが、アセットという追加のソフトウェアパッケージで処理や画像などの素材を追加することが出来ます。アセットはUnityのアセットストアで有償/無償で配布しています。無償の物でもクオリティーが高く便利なものがたくさんあります。今回はROSと通信ができる「ROS#」というアセットを使用します。
またUnityはマルチプラットフォームで実行できることが特徴です。Windowsの統合開発環境上でその場でプレビューすることができて、もちろんそれをWindowsアプリとしてリリースすることもがきます。それ以外にもAndroidやiPoneアプリとしてリリースすることもできます(ただしiPnone用のアプリ作成はMac上でのみ可能)。
Unityでは基本的にC#で開発を行います。UnityではC#プログラムを「スクリプト」と呼びます。
Unity自体に関する説明は長くなってしまうので、入門書にありそうな部分は説明を省きます。

ROS#について

UnityとROSの間を通信するためのUnityのアセットとしてROS#があります。これはC#で書かれたRosBridgeのクライアントでUnityでです。このアセットはUnityのアセットストアで配布しているものではなくgithubのページからダウンロードしてUnityでインポートします。
ROS#はrosbridge_serverがjson形式にしたrostopicをwebsocket通信でやり取りをすることでUnityとROSを接続します。

インストール(on Windows)

Unity本体のインストール

Unityのダウンロードページからダウンロードします。UnityHubはUnityのバージョンなどの管理ツールで最初は「UnityHub」をダウンロードします。「UnityHub」から「Unity本体」をダウンロードします。最新のROS#の推奨バージョンの2019.4を入れます。
後々必要なのでUnity本体のダウンロードの時に「Android Build Support」にチェックを入れてください。

プロジェクトの作成

UnityHubの「新規作成」から新しいプロジェクトを作成します。名前は何でも構いません。テンプレートは「3D」を選びます。
unity_basic_01.png

ROS#アセットのプロジェクトへのインストール

  1. github上のリリースページから「RosSharp.unitypackage」をダウンロードしてローカルに保存します。
  2. メニューバーの「Assets」->「Import Package」->「Custom Package...」を選んで出てくるウィンドウで「RosSharp.unitypackage」を選択します。
    unity_basic_02.png

  3. 出てくるウィンドウですべての項目にチェックが入っていることを確認して「import」 を押す。
    unity_basic_03.png

Unity上の操作(on Windows)

今回はROSからのimageのsubscribeとjoyのpublishを目標にします。

RosConnectorの設置

  • 「Hierarchy」ウィンドウの「+マーク」->「Create Empty」で空のGameObjectを作成して、RosConnectorと名前を変えます。
    unity_basic_04.png

  • 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/RosConnector.csをRosConnectorにアタッチします。

    • このスクリプトの「Ros Bridge Server Url」フィールドの値を{ROSを実行するPCのIPアドレス}:{Rosbridgeのポート}にします。 unity_basic_05.png

Imageのsubscribe

  • まず画像を表示するためのplaneを追加します。「Hierarchy」ウィンドウの「+マーク」->「3D Object」->「Plane」を選択します
    unity_basic_06.png

  • 位置(0,1,0)と姿勢(90,0,180)を調節します
    unity_basic_07.png

  • 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/ImageSubscriber.csをRosConnectorにアタッチします。

    • 「Topic」を「/head_camera/image_raw/compressed」と指定します。
    • 「Mesh Renderer」の横の三角形のマークをクリックして出てくる画面で先ほど作成したplaneを選択します。 unity_basic_08.png

Joyのpublish

  • 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/JoyPublisher.csをRosConnectorにアタッチします。
    • 「Topic」を/unity/joyとします。
  • 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/MessageHandling/JoyAxisReader.csをRosConnectorにアタッチします。
    • 「Name」を「Horizontal」とします。
  • もう1つ「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/MessageHandling/JoyAxisReader.csをRosConnectorにアタッチします。
    • 「Name」を「Vertical」とします。 unity_basic_09.png

実行

ネットワーク構成

ROSを実行するUbuntuPCとUnityを実行するWindowsPCが同一セグメント上にいるとします。
以下UbuntuPCのIPアドレスを「192.168.2.105」とします

ROSの実行(on Ubuntu)

シミュレーションを起動します。

ターミナル1(gazeboの起動)
roslaunch sim3_lecture base_world.launch 
ターミナル1(rosbridgeの起動)
roslaunch rosbridge_server rosbridge_websocket.launch

以下のようにgazwboが立ち上がります。
unity_basic_10.png

Unityの実行(on Windows)

UnityEditorのplay(上部中央の三角形のボタン)を押します。

unity_basic_11.png

設置したPlaneにカメラの画像が写っています。またROSでrostopic echo /unity/joyとすると、Unity側画面でのキーボードの上下左右ボタンの入力が反映されます。

コメント

このサンプルではpublish側でで不具合があるために動かない可能性があります。これの場合は以下の修正が必要です。

Assets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/JoyPublisher.csの修正
protected override void Start()
{
    System.Threading.Thread.Sleep(3000); // 追加
    base.Start();
    InitializeGameObject();
    InitializeMessage();
}

参考

目次ページへのリンク

ROS講座の目次へのリンク

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

自分用 XR リンク集

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

【Unity】アセットバンドルのビルドと読み込み時のハッシュ管理について

確認環境

Unity 2020.2.0f1

内容

自分自身アセットバンドルは使ってきましたが、サーバなどからダウンロードしてアセットバンドルの読み込みをしたことがありませんでした。
streamingassetsから読み込んでいるとハッシュを気にしなくてよかったのですが、キャッシュを聞かせるとなるとAssetBundleManifest(アセットバンドルをビルドしたときに勝手に作られるフォルダー名と同じアセットバンドル)のGetAssetBundleHashを使用してハッシュの管理をしてもよいですが、ビルドしたマシンの影響でハッシュが変わったり不安定で、jenkinsなど1か所でビルドしていてもjenkinsPC自体が変わってしまった場合にすべてハッシュが変わってしまうのではと思ったり、ダウンロードサイズを表示しないといけないなどがあったので独自にアセットバンドル一覧の管理アセットを用意する必要があったので備忘録的に残しました。

もしかしたらAddressableだとこの辺りもしっかりしているのかもしれませんがAddressableのことは何も調べていません。

準備

アセットバンドル名も独自に付加してもよかったのですが今回はAsset Bundle Browserを使用しています。
unity 2020.1からAsset Bundle Browserなどの一部のパッケージがPackage Manager(パッケージマネージャー)からインストールできなくなっているらしくこちらの記事を参考にインストールしました。

PackageManager01.PNG

com.unity.assetbundlebrowserを入力してAdd
PackageManager02.PNG

Window>AssetBundle Browserを開きアセットバンドルにしたアセットをドラッグ&ドロップでアセットバンドル名が付与されます。
assetbundlebrowser01.PNG

ビルド

Asset Bundle Browserを使用してそのままビルドもできますが。
今回はアセットのサイズ、ハッシュを管理できるアセット(ScriptableObject)も生成するので独自でビルドすることにしました。

ビルドの手順は次の通りです。
1. アセットバンドルをビルド
2. ビルド後該当のアセットバンドルのサイズ、ハッシュを計算して管理用のScriptableObjectを生成
3. 2で生成した管理用のScriptableObjectをアセットバンドル化

アセットバンドル管理用のハッシュについて

アセットバンドルのサイズはFileInfoのLengthでとれると思いますがハッシュをAssetBundleManifestのGetAssetBundleHashで取得した場合マシンによって変わり不安定なので自分で作る必要がありました。
調べるとCRCという元のアセットが変わった場合のみ変更される一貫性のある値が存在したので
アセットバンドル名+CRCをハッシュにすればいい感じになると思いこちらでハッシュを作ることにしました
結局CRCもOSによって変わってしまっているPrefabが存在しましたが・・・ 1

CRC

CRCなのですがこちらはアセットバンドルをダウンロードする場合に引数で渡さないといけないものです。
(よくわからないので0にしてスキップさせますが)これはマニュアルを見てみると

インテグリティチェックのためにダウンロードしたデータと比較するためのチェックサムです。

正常なデータのチェックに使われているみたいですが今回はこちらをハッシュに使用します。

取得自体は

BuildPipeline.GetCRCForAssetBundle(assetsPath, out uint crc)

で取得します。

ハッシュ生成

アセットバンドルのダウンロード時のキャッシュはUnityEngine.Hash128で管理しています。

namespace UnityEngine
{
    public struct Hash128 : IComparable, IComparable<Hash128>, IEquatable<Hash128>
    {
        public Hash128(ulong u64_0, ulong u64_1);
        public Hash128(uint u32_0, uint u32_1, uint u32_2, uint u32_3);
    }
}

生成にはuintやulongで生成する必要があったのでcrcはuintでそのまま取得できましたがアセットバンドル名(string)をuintにする必要がありました。

アプローチとしては
string→byte[]→uint
に変換するのですが
byte[]→uint
これをそのまま変換してしまうとuint.Maxを超える場合があったのでSha1に変換してからuintにしました。

SHA1Managed sha1 = new SHA1Managed();
byte[] textBytes = Encoding.UTF8.GetBytes("assetName");
byte[] sha1Bytes = sha1.ComputeHash(textBytes);
uint nameInt = BitConverter.ToUInt32(sha1Bytes, 0);
Hash128 hash128 = new Hash128(crc, nameInt);

使用例(UniTask)

    public static async UniTask<T> LoadAssetBundle<T>(AssetBundleLoadData assetBundleLoadData) where T : UnityEngine.Object
    {
        string assetName = assetBundleLoadData.AssetName;
        CachedAssetBundle cachedAssetBundle = new CachedAssetBundle(assetName, Hash128.Compute(assetBundleLoadData.Hash128));
        string uri = "ここはURL";
        using (var request = UnityWebRequestAssetBundle.GetAssetBundle(uri))
        {
            request.downloadHandler = new DownloadHandlerAssetBundle(uri, cachedAssetBundle, 0);
            await request.SendWebRequest();
            var assetBundle = DownloadHandlerAssetBundle.GetContent(request);
            T obj = assetBundle.LoadAsset<T>(assetName);
            assetBundle.Unload(false);
            return obj as T;
        }
    }

今回のサンプルコード

アセットバンドルのサンプルビルドコード
AssetsBundleBuilder.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;

public class AssetsBundleBuilder
{
    [MenuItem("Sample/AssetBundleBuild")]
    private static void AssetBundleBuild()
    {
        BuildTarget buildTarget = BuildTarget.Android;
        BuildAssetBundleOptions buildAssetBundleOptions = BuildAssetBundleOptions.DeterministicAssetBundle;
        string platformName = buildTarget.ToString().ToLower();
        string assetbundledatalistName = "assetbundledatalist" + platformName;
        string outputFolderPath = Path.Combine(Application.dataPath, "Scenes/AssetBundleScene/Output");     // 出力フォルダ
        string outputAssetbundlesFolderPath = Path.Combine(outputFolderPath, platformName);     // アセットバンドルビルド出力フォルダ
        CheckDirectory(outputFolderPath);
        CheckDirectory(outputAssetbundlesFolderPath);


        string[] assetbundleNames = AssetDatabase.GetAllAssetBundleNames();
        List<AssetBundleBuild> assetBundleBuildList = new List<AssetBundleBuild>();

        foreach (var item in assetbundleNames)
        {
            // アセットバンドル管理用のアセットバンドルは別でビルドする
            if (item.Contains(assetbundledatalistName)) continue;
            string[] assetsPaths = AssetDatabase.GetAssetPathsFromAssetBundle(item);
            AssetBundleBuild assetBundleBuild = new AssetBundleBuild();
            assetBundleBuild.assetBundleName = item;
            assetBundleBuild.assetNames = assetsPaths;
            assetBundleBuildList.Add(assetBundleBuild);
        }
        AssetBundleManifest assetBundleManifest = BuildPipeline.BuildAssetBundles(outputAssetbundlesFolderPath, assetBundleBuildList.ToArray(), buildAssetBundleOptions, buildTarget);


        // コンバート先のフォルダーからハッシュタグとbyteサイズを計算して。
        AssetBundleLoadDataList assetBundleLoadDataList = ScriptableObject.CreateInstance<AssetBundleLoadDataList>();
        foreach (var item in assetBundleManifest.GetAllAssetBundles())
        {
            FileInfo file = new FileInfo(Path.Combine(outputAssetbundlesFolderPath, item));
            long size = file.Length;
            Hash128 hash128 = GenerateHash128AssetNameCRC(outputAssetbundlesFolderPath, item, assetBundleManifest);
            assetBundleLoadDataList.SetAssetBundleLoadData(new AssetBundleLoadData(item, hash128, (uint)size));
        }


        // ScriptableObjectの作成はAssetsからのパスなので注意
        string filePath = Path.Combine(@"Assets/Scenes/AssetBundleScene/Output", assetbundledatalistName + ".asset");
        CheckDirectory(Path.GetDirectoryName(filePath));
        AssetDatabase.DeleteAsset(filePath);
        AssetDatabase.CreateAsset(assetBundleLoadDataList, filePath);
        AssetImporter importer = AssetImporter.GetAtPath(filePath);
        importer.assetBundleName = assetbundledatalistName;
        importer.SaveAndReimport();
        EditorUtility.SetDirty(assetBundleLoadDataList);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        AssetBundleBuild[] assetBundleBuildArray = new AssetBundleBuild[1];
        assetBundleBuildArray[0].assetBundleName = assetbundledatalistName;
        assetBundleBuildArray[0].assetNames = new string[] { filePath };
        BuildPipeline.BuildAssetBundles(outputAssetbundlesFolderPath, assetBundleBuildArray, buildAssetBundleOptions, buildTarget);

    }

    /// <summary>
    /// アセットバンドル名とCRCからアセットバンドルのハッシュを生成
    /// </summary>
    /// <param name="outputFolder"></param>
    /// <param name="assetName"></param>
    /// <param name="assetBundleManifest"></param>
    /// <returns></returns>
    private static Hash128 GenerateHash128AssetNameCRC(string outputFolder, string assetName, AssetBundleManifest assetBundleManifest)
    {
        string assetsPath = Path.Combine(outputFolder, assetName);
        if (BuildPipeline.GetCRCForAssetBundle(assetsPath, out uint crc))
        {
            SHA1Managed sha1 = new SHA1Managed();
            byte[] textBytes = Encoding.UTF8.GetBytes(assetName);
            byte[] sha1Bytes = sha1.ComputeHash(textBytes);
            uint nameInt = BitConverter.ToUInt32(sha1Bytes, 0);
            Hash128 hash128 = new Hash128(crc, nameInt);
            return hash128;
        }
        else
        {
            return assetBundleManifest.GetAssetBundleHash(assetName);
        }
    }

    private static void CheckDirectory(string outputFolder)
    {
        if (Directory.Exists(outputFolder)) return;
        Directory.CreateDirectory(outputFolder);
    }
}


AssetBundleLoadDataList.cs(管理用のScriptableObjectクラス)
AssetBundleLoadDataList.cs
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class AssetBundleLoadDataList : ScriptableObject
{
    [SerializeField]
    private List<AssetBundleLoadData> m_AssetBundleLoadDataList;

    public AssetBundleLoadDataList()
    {
        m_AssetBundleLoadDataList = new List<AssetBundleLoadData>();
    }

    public void SetAssetBundleLoadData(AssetBundleLoadData assetBundleLoadData)
    {
        m_AssetBundleLoadDataList.Add(assetBundleLoadData);
    }

    public List<AssetBundleLoadData> GetAssetBundleLoadDataList()
    {
        return m_AssetBundleLoadDataList;
    }
}

[Serializable]
public class AssetBundleLoadData
{
    public string AssetName;
    public string Hash128;
    public uint Bytes;

    public AssetBundleLoadData(string assetName, Hash128 hash128, uint bytes)
    {
        AssetName = assetName;
        Hash128 = hash128.ToString();
        Bytes = bytes;
    }


    public override string ToString()
    {
        return $"<color=red>{AssetName}</color>\nHash128[ {Hash128} ]\nBytes[ {Bytes} ]";
    }
}


おわり

様々な管理方法があるかと思います、今回初めてだったこともありもっと良い方法などもあるかと思いますが。
これからアセットバンドルのロードなどを実装する方たちに何かしらの参考になればと思います。


  1. 詳しくは調べられませんでしたがSpineなどのアセットはwindowsとmacでアセットバンドルをビルドした場合でもcrcが変更しました。同じOS内なら変更されなかったのでビルドするOSは統一させないといけなかったです。 

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

【Unity】RendererFeatureの値をランタイムで変更したい

URPの場合は以下のようにRendererFeatureにアクセスできます。

var pipeline = ((UniversalRenderPipelineAsset)GraphicsSettings.renderPipelineAsset);
var propertyInfo = pipeline.GetType().GetField("m_RendererDataList", BindingFlags.Instance | BindingFlags.NonPublic);
var scriptableRendererData = ((ScriptableRendererData[])propertyInfo.GetValue(pipeline))[0];
var renderObjects = (RenderObjects)scriptableRendererData.rendererFeatures[index];

renderObjects.settings.depthCompareFunction = CompareFunction.Greater;
scriptableRendererData.SetDirty();

値を変更した場合、ScriptableRendererData.SetDirty() を呼び出すことで変更が反映されます。

RendererFeatureChange_gif.gif

自作RendererFeatureを実装している場合は以下のようにアクセスできます。

var myRendererFeature = scriptableRendererData.rendererFeatures.OfType<MyRendererFeature>().FirstOrDefault();

Unity 2020.1.17f1
Universal RP 8.3.1

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

【Unity】カスタムポストエフェクトでURP入門してみる(ぼかし処理)

はじめに

最近のUnityの傾向として、かつてからある伝統的なBuilt-in Render Pipelineから、Universal Render Pipeline(URP)に移行せよ...という圧力を感じます。Built-in Render Pipelineを選択するのもアリだよとも聞くので、悩みどころとなります。

一方自分は実務ではここ1年程Unity 2019/Built-in Redner Pipelineを開発で使ってきました。
Unity2018の頃はLWRPと呼ばれていましたが採用せず(正直に言えばこれは正解だったと思う)、後にURPと改名されたことを知りまだまだ安定してないのでは等の不安感が強く、実務での利用を控えてきました。
なによりただでさえ遅れがちなゲーム開発の進捗が遅れることを恐れたためです。

しかしながらShader Graph使いたい、というデザイナーさんからの要望も高まっており(現に使っていてデザイナーさん凄いなと思うのですが)、これがURPでないと利用できないためShader Graphを使いたいがために「各社頑張って対応してそうだな」などと想像しています。

また、先日Unity 2020がLTSとなったこともあり、このままではゲームプログラマとして不味いという強迫観念もあり、いよいよ自分もBuilt-in Render Pipelineを捨ててURPに乗り換える気になってきた、というのが最近の状況です。(つまりURP完全初心者!)

調査がてらポストエフェクトないしブラー処理から入門してみることにしました。ブラー処理は手軽に実装できかつ比較的よくある要望の1つであるためです。なによりポストエフェクトにはロマンがある。

なお、ここでいうブラー処理とは移動ベクトルを使うモーションブラーのことではなく、画像処理でいうところのガウシアンフィルタのようなぼかし処理です。

手始めに先人の知恵を借りる

まずどこから手をつけたら良いか分からなかったので、ググったところ下記の2つが大変参考になりました。

Unity HubからUniversal Render Pipelineテンプレートから新規プロジェクトを作成して上記の2つのRenderFeatureを組み込んでみたのが下記。

ちょっとバトルシーンに入る前のエフェクトみたいになって楽しいです。やっぱりポストエフェクトにはロマンがある!
これはこれで素晴らしいのですが当初やりたかったのは、ぼかし処理なのだ、ということを思い出す。

UnityのPost Processing Stackについて

Unity 2018を触っていた頃Post Processing Stack v2というものがあることを知り、これを使えば比較的簡単にポストエフェクトを実装できる凄いやつ、程度の認識をしていました(元々はPostFx v2というものだったそうです)。

さらに遡るとUnity 5.xの頃のUnityマニュアルを見ると、この頃はそもそもデフォルトでBlurがあったようです。
当時はRenderTextureベースでスクリーンに対するImage Effectという位置付けで実装されていたようです。

一方Post Processing Stack v2のリファレンスのEffectsを見るとでは、カメラの動きによるモーションブラーは実装されているものの、ガウシアンフィルタのようなものはありません。
Post Processing Stack v2でこれを自前実装することなしに実現したい場合はUnityToolsetを導入する必要があるとのことでした。

なるほど、一体何故かつて提供していたにも関わらずデフォルトで実装されていないのか。
それは自分で実装してくださいよ、というメッセージなのだろうか。やりましょう。

さらにURPの進展からかPost Processing Stack v2はレガシーとのこと。僅か2年程でレガシーというのが何ともアレなのですが、Post Processing Stack v3に引き継がれる流れのようです。消えなくて良かった。

では、Post Processing Stack v3ではどうなのかというと、Unity Forumでも同様の質問があり、特に将来的に実装予定でもなさそうです。代わりにURPのScriptableRenderFeatureで実装したデモがこのフォーラムで紹介されており、参考になりそうです。

やりたかったのはコレですよコレ!勉強になります!(新しいオモチャを見つけた子供のような心で)

デモの内容を見る

デモを見ると分かるのですが、このデモではblurマテリアルにCustom/RenderFeature/KawaseBlurシェーダ、glassマテリアルにShaderGraphs/BathroomGlassシェーダが付けれており、グラス表現を適用したPlaneに対して川瀬ブラーを施す、というものです。

初見どこを弄ればブラー効果が確認できるのか分からなかったのですが、ForwardRendererでプロパティが露出していました。

手始めに先人の知恵を借りるでは共にVolumeComponentを継承したクラスでプロパティが実装されておりVolumeコンポーネントのインスペクターにプロパティが露出していたため多少混乱しました。

一方こちらのデモではVolume framework(URP 10.1.0以前ではVolume systemと書かれていた)は関係なく、KawaseBlurSettingsScriptableRendererFeature内で定義されており、これが設定項目としてFowardRendererに露出しているようです。

kawase_blur_inspector.png

各設定項目を見てみましょう。

Render Pass Event

Rendear Passを差し込むタイミングの指定です。
どのタイミングが良いかはRenderPassEvent.BeforeRenderingPostProcessingもしくはRenderPassEvent.AfterRenderingTransparentsあたりかな、と思いました。

Blur Material

KawaseBlurに紐づくマテリアルを指定します。

Blur Passes

ブラーの強さです。値を大きくすればするほど強いブラーとなりますが、負荷が気になるところです。

Downsample

負荷低減のためダウンサンプリングする際のテクスチャ縮小具合です。
下記の計算でテクスチャを縮小しているようです。つまり2を指定すればテクスチャサイズは1/4となりその分負荷が軽くなりそうです。

var width = cameraTextureDescriptor.width / downsample;
var height = cameraTextureDescriptor.height / downsample;

ある程度負荷を抑える制御ができる点が玄人的で素晴らしいです。

Copy To Framebuffer

このデモはBathroomGlass.shadergraphで参照しているテクスチャに対してブラーをかけるだけかと思いきやこのオプションを有効にするとPlaneだけでなくスクリーン全体にブラーが効きました(これが本当にやりたかったこと)。

Target Name

_blurTextureという文字列が指定されています。
これはBathroomGlass.shadergraphで参照されるプロパティのようです。
この辺りはまだShaderGraphを調査していないため雰囲気で言っています。
BathroomGlass.shadergraphのインスペクターで「Edit」を押すとShaderGraphのエディタが開き、中身を確認できました。

_blurTexture.png

なおKawaseShader.shaderについては何故かmulti_compile_fogというpragmaがあり本当にコレが必要なのかどうなのかは良く分かりませんでした(フォーラムでも質問が上がっているが特に返答は得られていない様子)。

川瀬ブラーのみ取り出してVolumeComponentで扱えるようにする

上記のデモがあれば十分なのですが、このままだとVolume frameworkに適応していません。

Volume frameworkに則るとweightで「ポストエフェクトがどの程度影響するか」を調整できるため、便利そうです。
また、volume.profile.TryGet<MyVolumeComponent>(out var volumeComponent)のように自作したVolumeComponentにアクセスしてプロパティをプログラムから制御できるため、都合が良さそうです。

そこで手始めに先人の知恵を借りるで紹介されているような方法で書き直してみます。
今回スクリーン全体に適用されたら十分だったため必要なさそうな箇所は適当に調整して簡略化しました。

KawaseBlur.cs
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class KawaseBlur : VolumeComponent, IPostProcessComponent
{
    // RangeではなくClampedIntParameterを使うことでインスペクター上でスライダーとなることに注意。
    public ClampedIntParameter passes     = new ClampedIntParameter(2, 2, 20);
    public IntParameter        downsample = new ClampedIntParameter(1, 1, 10);

    public bool IsActive() => passes.value >= 2;
    public bool IsTileCompatible() => false;
}
KawaseBlurRenderFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class KawaseBlurRenderFeature : ScriptableRendererFeature
{
    class CustomRenderPass : ScriptableRenderPass
    {
        static readonly string RenderTag = "Kawase Blur Effect";

        KawaseBlur kawaseBlur = default;
        Material   material   = default;

        RenderTargetIdentifier source = default;
        RenderTargetIdentifier tmpRT1 = default;
        RenderTargetIdentifier tmpRT2 = default;

        int tmpId1 = default;
        int tmpId2 = default;

        public CustomRenderPass(RenderPassEvent evt)
        {
            renderPassEvent = evt;
            var shader = Shader.Find("Custom/RenderFeature/KawaseBlur");
            if (shader == default)
            {
                return;
            }
            material = CoreUtils.CreateEngineMaterial(shader);
        }

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

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            if (kawaseBlur == default)
            {
                var stack = VolumeManager.instance.stack;
                kawaseBlur = stack.GetComponent<KawaseBlur>();
            }

            if (!IsValid()) return;

            var downsample = kawaseBlur.downsample.value;
            var width      = cameraTextureDescriptor.width / downsample;
            var height     = cameraTextureDescriptor.height / downsample;

            tmpId1 = Shader.PropertyToID("tmpBlurRT1");
            tmpId2 = Shader.PropertyToID("tmpBlurRT2");
            cmd.GetTemporaryRT(tmpId1, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);
            cmd.GetTemporaryRT(tmpId2, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);

            tmpRT1 = new RenderTargetIdentifier(tmpId1);
            tmpRT2 = new RenderTargetIdentifier(tmpId2);

            ConfigureTarget(tmpRT1);
            ConfigureTarget(tmpRT2);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (!renderingData.cameraData.postProcessEnabled) return;
            if (!IsValid()) return;

            var passes = kawaseBlur.passes.value;

            CommandBuffer cmd = CommandBufferPool.Get(RenderTag);

            RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
            opaqueDesc.depthBufferBits = 0;

            // first pass
            cmd.SetGlobalFloat("_offset", 1.5f);
            cmd.Blit(source, tmpRT1, material);

            for (var i = 1; i < passes - 1; i++) {
                cmd.SetGlobalFloat("_offset", 0.5f + i);
                cmd.Blit(tmpRT1, tmpRT2, material);

                // pingpong
                var rttmp = tmpRT1;
                tmpRT1 = tmpRT2;
                tmpRT2 = rttmp;
            }

            // final pass
            cmd.SetGlobalFloat("_offset", 0.5f + passes - 1f);
            cmd.Blit(tmpRT1, source, material);

            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            CommandBufferPool.Release(cmd);
        }

        bool IsValid()
        {
            if (material == default)
            {
                Debug.LogError("material is not found.");
                return false;
            }

            if (kawaseBlur == default)
            {
                Debug.LogError("kawaseBlur is not found.");
                return false;
            }

            return kawaseBlur.IsActive();
        }
    }

    CustomRenderPass scriptablePass = default;

    public override void Create()
    {
        scriptablePass = new CustomRenderPass(RenderPassEvent.AfterRenderingTransparents);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        scriptablePass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(scriptablePass);
    }
}
KawaseBlur.shader
Shader "Custom/RenderFeature/KawaseBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex   vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv     : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv     : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4    _MainTex_TexelSize;
            float4    _MainTex_ST;
            float     _offset;

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

            fixed4 frag (v2f input) : SV_Target
            {
                float2 res = _MainTex_TexelSize.xy;
                float  i   = _offset;
                fixed4 col = float4(0, 0, 0, 1);

                col.rgb  = tex2D(_MainTex, input.uv ).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2( i,  i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2( i, -i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2(-i,  i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2(-i, -i) * res).rgb;
                col.rgb /= 5.0f;

                return col;
            }
            ENDCG
        }
    }
}

Editor上では下記のようなことまでできました。(解像度的に見辛いかもしれませんが...)

実機検証

普段はiOS/Androidで開発していますが、AndroidではVukkan/OpenGL ESの差でハマりがちなので、警戒が必要です。

手元のiPhone 12, Google Pixel XL(Vulkan)では無事動作しましたが、Open GL ES 3.0にフォールバックされた場合は手元に検証端末がなく、確認していません。

まとめ

簡単なポストエフェクト実装からURPに入門してみました。

Scriptable Rendering Pipelineまわりは開発が活発で次から次へと状況が変わっており、ただでさえ敷居の高いGraphicsに拍車をかけて追いかけるのが難しいです。

また作業中何かの拍子にGameViewに何も映らなくなりUnity再起動で直ったり、VolumeComponentで公開するClampedIntParameterのmin,maxの値を変更して再コンパイルしたときに正常に反映されず一度VolumeComponentを外して付け直すと直る、見慣れない警告が出たりといったような事が発生して少し驚きます。

とはいえ最近はモバイルゲームといえど、ハイクオリティなタイトルも珍しくなくなってきているため、今後もURPへの期待は業界内で高まっていくのが自然な流れかなと思っています。

ある程度手応えを得ることができたので、これを皮切りに今後もリファレンスやサンプルを読み漁りつつ追いかけてみようと思います。

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

【Unity】カスタムポストエフェクトでURPに入門してみる(ぼかし処理)

はじめに

最近のUnityの傾向として、かつてからある伝統的なBuilt-in Render Pipelineから、Universal Render Pipeline(URP)に移行せよ...という圧力を感じます。Built-in Render Pipelineを選択するのもアリだよとも聞くので、悩みどころとなります。

一方自分は実務ではここ1年程Unity 2019/Built-in Redner Pipelineを開発で使ってきました。
Unity2018の頃はLWRPと呼ばれていましたが採用せず(正直に言えばこれは正解だったと思う)、後にURPと改名されたことを知りまだまだ安定してないのでは等の不安感が強く、実務での利用を控えてきました。なによりただでさえ遅れがちなゲーム開発の進捗が遅れることを恐れたためです。

しかしながらShader Graph使いたい、というデザイナーさんからの要望も高まっており(現に使っていてデザイナーさん凄いなと思うのですが)、これがURPでないと利用できないためShader Graphを使いたいがために「各社頑張って対応してそうだな」などと想像しています。

また、先日Unity 2020がLTSとなったことを受け「このままではゲームプログラマとして不味い」という強迫観念を感じ、いよいよ自分もBuilt-in Render Pipelineを捨ててURPに乗り換える気になってきた、というのが最近の状況です。(つまりURP完全初心者!)

調査がてらポストエフェクトないしブラー処理から入門してみることにしました。ブラー処理は手軽に実装できかつ比較的よくある要望の1つであるためです。なによりポストエフェクトにはロマンがある。

なお、ここでいうブラー処理とは移動ベクトルを使うモーションブラーのことではなく、画像処理でいうところのガウシアンフィルタのようなぼかし処理です。

実行環境

Unity: 2020.3.0f1
Core RP Library: 10.3.2
Universal RP: 10.3.2

手始めに先人の知恵を借りる

まずどこから手をつけたら良いか分からなかったので、ググったところ下記の2つが大変参考になりました。

Unity HubからUniversal Render Pipelineテンプレートから新規プロジェクトを作成して上記の2つのRenderFeatureを組み込んでみたのが下記。

ちょっとバトルシーンに入る前のエフェクトみたいになって楽しいです。やっぱりポストエフェクトにはロマンがある!
これはこれで素晴らしいのですが当初やりたかったのは、ぼかし処理なのだ、ということを思い出す。

UnityのPost Processing Stackについて

Unity 2018を触っていた頃Post Processing Stack v2というものがあることを知り、これを使えば比較的簡単にポストエフェクトを実装できる凄いやつ、程度の認識をしていました(元々はPostFx v2というものだったそうです)。

さらに遡るとUnity 5.xの頃のUnityマニュアルを見ると、この頃はそもそもデフォルトでBlurがあったようです。当時はRenderTextureベースでスクリーンに対するImage Effectという位置付けで実装されていたようです。

一方Post Processing Stack v2のリファレンスのEffectsを見るとでは、カメラの動きによるモーションブラーは実装されているものの、ガウシアンフィルタのようなものはありません。
Post Processing Stack v2でこれを自前実装することなしに実現したい場合はUnityToolsetを導入する必要があるとのことでした。

なるほど、一体何故かつて提供していたにも関わらずデフォルトで実装されていないのか。
それは自分で実装してくださいよ、というメッセージなのだろうか。やりましょう。

さらにURPの進展からかPost Processing Stack v2はレガシーとのこと。僅か2年程でレガシーというのが何ともアレなのですが、Post Processing Stack v3に引き継がれる流れのようです。消えなくて良かった。

では、Post Processing Stack v3ではどうなのかというと、Unity Forumでも同様の質問があり、特に将来的に実装予定でもなさそうです。代わりにURPのScriptableRenderFeatureで実装したデモがこのフォーラムで紹介されており、参考になりそうです。

やりたかったのはコレですよコレ!勉強になります!(新しいオモチャを見つけた子供のような心で)

デモの内容を見る

デモを見ると分かるのですが、このデモではblurマテリアルにCustom/RenderFeature/KawaseBlurシェーダ、glassマテリアルにShaderGraphs/BathroomGlassシェーダが付けれており、グラス表現を適用したPlaneに対して川瀬ブラーを施す、というものです。

初見どこを弄ればブラー効果が確認できるのか分からなかったのですが、ForwardRendererでプロパティが露出していました。

手始めに先人の知恵を借りるでは共にVolumeComponentを継承したクラスでプロパティが実装されておりVolumeコンポーネントのインスペクターにプロパティが露出していたため多少混乱しました。

一方こちらのデモではVolume framework(URP 10.1.0以前ではVolume systemと書かれていた)は関係なく、KawaseBlurSettingsScriptableRendererFeature内で定義されており、これが設定項目としてFowardRendererに露出しているようです。

kawase_blur_inspector.png

各設定項目を見てみましょう。

Render Pass Event

Render Passを差し込むタイミングの指定です。
どのタイミングが良いかはRenderPassEvent.BeforeRenderingPostProcessingもしくはRenderPassEvent.AfterRenderingTransparentsあたりかな、と思いました。

Blur Material

KawaseBlurに紐づくマテリアルを指定します。

Blur Passes

ブラーの強さです。値を大きくすればするほど強いブラーとなりますが、負荷が気になるところです。

Downsample

負荷低減のためダウンサンプリングする際のテクスチャ縮小具合です。
下記の計算でテクスチャを縮小しているようです。つまり2を指定すればテクスチャサイズは1/4となりその分負荷が軽くなりそうです。

var width = cameraTextureDescriptor.width / downsample;
var height = cameraTextureDescriptor.height / downsample;

ある程度負荷を抑える制御ができる点が玄人的で素晴らしいです。

Copy To Framebuffer

このデモはBathroomGlass.shadergraphで参照しているテクスチャに対してブラーをかけるだけかと思いきやこのオプションを有効にするとPlaneだけでなくスクリーン全体にブラーが効きました(これが本当にやりたかったこと)。

Target Name

_blurTextureという文字列が指定されています。
これはBathroomGlass.shadergraphで参照されるプロパティのようです。
この辺りはまだShaderGraphを調査していないため雰囲気で言っています。
BathroomGlass.shadergraphのインスペクターで「Edit」を押すとShaderGraphのエディタが開き、中身を確認できました。

_blurTexture.png

なおKawaseShader.shaderについては何故かmulti_compile_fogというpragmaがあり本当にコレが必要なのかどうなのかは良く分かりませんでした(フォーラムでも質問が上がっているが特に返答は得られていない様子)。

川瀬ブラーのみ取り出してVolumeComponentで扱えるようにする

上記のデモがあれば十分なのですが、このままだとVolume frameworkに適応していません。

Volume frameworkに則るとweightで「ポストエフェクトがどの程度影響するか」を調整できるため、便利そうです。
また、volume.profile.TryGet<MyVolumeComponent>(out var volumeComponent)のように自作したVolumeComponentにアクセスしてプロパティをプログラムから制御できるため、都合が良さそうです。

そこで手始めに先人の知恵を借りるで紹介されているような方法で書き直してみます。
今回スクリーン全体に適用されたら十分だったため必要なさそうな箇所は適当に調整して簡略化しました。

KawaseBlur.cs
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class KawaseBlur : VolumeComponent, IPostProcessComponent
{
    // RangeではなくClampedIntParameterを使うことでインスペクター上でスライダーとなることに注意。
    public ClampedIntParameter passes     = new ClampedIntParameter(2, 2, 20);
    public IntParameter        downsample = new ClampedIntParameter(1, 1, 10);

    public bool IsActive() => passes.value >= 2;
    public bool IsTileCompatible() => false;
}
KawaseBlurRenderFeature.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class KawaseBlurRenderFeature : ScriptableRendererFeature
{
    class CustomRenderPass : ScriptableRenderPass
    {
        static readonly string RenderTag = "Kawase Blur Effect";

        KawaseBlur kawaseBlur = default;
        Material   material   = default;

        RenderTargetIdentifier source = default;
        RenderTargetIdentifier tmpRT1 = default;
        RenderTargetIdentifier tmpRT2 = default;

        int tmpId1 = default;
        int tmpId2 = default;

        public CustomRenderPass(RenderPassEvent evt)
        {
            renderPassEvent = evt;
            var shader = Shader.Find("Custom/RenderFeature/KawaseBlur");
            if (shader == default)
            {
                return;
            }
            material = CoreUtils.CreateEngineMaterial(shader);
        }

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

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            if (kawaseBlur == default)
            {
                var stack = VolumeManager.instance.stack;
                kawaseBlur = stack.GetComponent<KawaseBlur>();
            }

            if (!IsValid()) return;

            var downsample = kawaseBlur.downsample.value;
            var width      = cameraTextureDescriptor.width / downsample;
            var height     = cameraTextureDescriptor.height / downsample;

            tmpId1 = Shader.PropertyToID("tmpBlurRT1");
            tmpId2 = Shader.PropertyToID("tmpBlurRT2");
            cmd.GetTemporaryRT(tmpId1, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);
            cmd.GetTemporaryRT(tmpId2, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);

            tmpRT1 = new RenderTargetIdentifier(tmpId1);
            tmpRT2 = new RenderTargetIdentifier(tmpId2);

            ConfigureTarget(tmpRT1);
            ConfigureTarget(tmpRT2);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (!renderingData.cameraData.postProcessEnabled) return;
            if (!IsValid()) return;

            var passes = kawaseBlur.passes.value;

            CommandBuffer cmd = CommandBufferPool.Get(RenderTag);

            RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
            opaqueDesc.depthBufferBits = 0;

            // first pass
            cmd.SetGlobalFloat("_offset", 1.5f);
            cmd.Blit(source, tmpRT1, material);

            for (var i = 1; i < passes - 1; i++) {
                cmd.SetGlobalFloat("_offset", 0.5f + i);
                cmd.Blit(tmpRT1, tmpRT2, material);

                // pingpong
                var rttmp = tmpRT1;
                tmpRT1 = tmpRT2;
                tmpRT2 = rttmp;
            }

            // final pass
            cmd.SetGlobalFloat("_offset", 0.5f + passes - 1f);
            cmd.Blit(tmpRT1, source, material);

            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            CommandBufferPool.Release(cmd);
        }

        bool IsValid()
        {
            if (material == default)
            {
                Debug.LogError("material is not found.");
                return false;
            }

            if (kawaseBlur == default)
            {
                Debug.LogError("kawaseBlur is not found.");
                return false;
            }

            return kawaseBlur.IsActive();
        }
    }

    CustomRenderPass scriptablePass = default;

    public override void Create()
    {
        scriptablePass = new CustomRenderPass(RenderPassEvent.AfterRenderingTransparents);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        scriptablePass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(scriptablePass);
    }
}
KawaseBlur.shader
Shader "Custom/RenderFeature/KawaseBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderPipeline" = "UniversalPipeline" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex   vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv     : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv     : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4    _MainTex_TexelSize;
            float4    _MainTex_ST;
            float     _offset;

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

            fixed4 frag (v2f input) : SV_Target
            {
                float2 res = _MainTex_TexelSize.xy;
                float  i   = _offset;
                fixed4 col = float4(0, 0, 0, 1);

                col.rgb  = tex2D(_MainTex, input.uv ).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2( i,  i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2( i, -i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2(-i,  i) * res).rgb;
                col.rgb += tex2D(_MainTex, input.uv + float2(-i, -i) * res).rgb;
                col.rgb /= 5.0f;

                return col;
            }
            ENDCG
        }
    }
}

Editor上では下記のようなことまでできました。(解像度的に見辛いかもしれませんが...)

実機検証

普段はiOS/Androidで開発していますが、AndroidではVukkan/OpenGL ESの差でハマりがちなので、警戒が必要です。

手元のiPhone 12, Google Pixel XL(Vulkan)では無事動作しましたが、Open GL ES 3.0にフォールバックされた場合は手元に検証端末がなく、確認していません。

まとめ

簡単なポストエフェクト実装からURPに入門してみました。

Scriptable Rendering Pipelineまわりは開発が活発で次から次へと状況が変わっており、ただでさえ敷居の高いGraphicsに拍車をかけて追いかけるのが難しいです。

また作業中何かの拍子にGameViewに何も映らなくなりUnity再起動で直ったり、VolumeComponentで公開するClampedIntParameterのmin,maxの値を変更して再コンパイルしたときに正常に反映されず一度VolumeComponentを外して付け直すと直る、見慣れない警告が出たりといったような事が発生して少し驚きます。

とはいえ最近はモバイルゲームといえど、ハイクオリティなタイトルも珍しくなくなってきているため、今後もURPへの期待は業界内で高まっていくのが自然な流れかなと思っています。

ある程度手応えを得ることができたので、これを皮切りに今後もリファレンスやサンプルを読み漁りつつ追いかけてみようと思います。

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

Caching.ClearCacheでキャッシュの削除に失敗してfalseが返ってくる

事象
・Caching.ClearCacheでキャッシュの削除に失敗してfalseが返ってくる
・アセットバンドルのダウンロード後すべて失敗

再現方法

  1. アセットバンドルダウンロード
  2. Caching.ClearCache()実行
  3. アセットバンドルダウンロード
  4. Caching.ClearCache()実行(失敗!!!!)

ClearCacheする直前に AssetBundle.UnloadAllAssetBundles(false); を呼んであげると解決

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

【Unity】スマホアプリでカメラロールへの保存・SNSにシェアができるアセット NatShareのメモ

はじめに

【Unity】簡単にビデオ録画ができるアセット NatCorderのメモという記事の続きになります。
https://qiita.com/Formi/items/4b1ea2c53e25033436ea

スマホアプリ内で作成した画像や動画ファイルをカメラロールへ保存・SNSへシェアしたかったので利用しました。
NatCorderと違い無料のAssetになのがありがたいですね!

NatShare

NatShare は、軽量で使いやすい Unity Engine 用のソーシャル共有 API です。NatShare を使用すると、わずか 3 つのステップでクロスプラットフォームのテキスト、画像、メディアの共有が可能になります。

公式API
https://docs.natsuite.io/natshare/
スクリーンショット 2021-03-07 23.41.06.png

こちらからインポートできます。
https://assetstore.unity.com/packages/tools/integration/natshare-mobile-sharing-api-117705

Share方法

SharePayloadクラス

SharePayloadクラスを生成します。
名前の通りこのオブジェクトを使ってシェアをすることになります。

var sharePayload = new SharePayload();

SharePayload.AddText

シェアするときに利用するテキストを設定する関数です。
引数にはstring型で渡してあげます。

sharePayload.AddText(stringText);

SharePayload.AddMedia

シェアしたい「音声・画像・ビデオ」ファイルを設定する関数です。
引数にはstring型でpathを渡してあげます。

sharePayload.AddMedia(mediaPath);

SharePayload.AddImage

シェアしたい「画像」ファイルを設定する関数です。
引数にはTexture2D型で直接ファイルを指定します。(NatCorderと合わせて使う場合はpathが吐き出されるのでAddMediaでいい気がします)

sharePayload.AddMedia(mediaPath);

SharePayload.Commit

上記関数で設定した情報をもとにShereをします。
プラットフォーム判別はいい感じにやってくれるので気にしなくても良いです。

sharePayload.Commit();

カメラロールへの保存方法

SavePayloadクラス

SharePayloadクラスを生成します。
オブジェクト生成時に保存するフォルダー名を指定します。
用意されている関数がSherePayLoadと同じなので簡単に使えます!

var savePayload = new SavePayload(FOLDER_NAME);

SavePayload.AddMedia

シェアしたい「音声・画像・ビデオ」ファイルを設定する関数です。
引数にはstring型でpathを渡してあげます。

savePayload.AddMedia(mediaPath);

SavePayload.AddImage

シェアしたい「画像」ファイルを設定する関数です。
引数にはTexture2D型で直接ファイルを指定します。

savePayload.AddMedia(mediaPath);

SavePayload.Commit

上記関数で設定した情報をもとにカメラロールでフォルダの作成・保存をします。
プラットフォーム判別はこいつがいい感じにやってくれるので気にしなくても良いです。

savePayload.Commit();

使い方

使いたいクラスのオブジェクトにAddしてCommitするだけで使えます。
Commit関数はTaskになっていて成功したかどうかをreturnしてくれるので、ちゃんと使いたい方は公式APIをのぞいてみてください。

まとめ

NatCorderと合わせて使うことでMediaを扱う幅がグッと広がりました!
APIもしっかり用意されていて神Assetだと思います。

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

【Unity】ステートごとにapplyRootMotionを設定する方法

こんな困りごとありませんか?

移動中は rigidbody で座標を操作したいけど、
この攻撃モーション中は applyRootMotion で座標制御しないと不自然…

よし、キャラに付けてるスクリプトの Update で
ステート名が「SpecialAttack」なら applyRootMotion を true にして
ステート名が「Locomotion」なら false にしよう!

あ、そうや、「Jump」ステートも applyRootMotion = true で「Fall」は false やな…
あれもこれも分岐に追加や…

あーもうぐちゃぐちゃだよ(´・ω・`)

その悩み、解決します!

これでスッキリ!

事前準備として、applyRootMotion の初期値は false にしておきましょう。
image.png

つづいて applyRootMotion = true にしたいステートを選択して~
image.png

Inspector の一番下「Add Behaviour」から「New Script > ApplyRootMotion」を選択します。
image.png

中身はこんなかんじ

ApplyRootMotion.cs
using UnityEngine;

public class ApplyRootMotion : StateMachineBehaviour
{
    //OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.applyRootMotion = true;
    }

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.applyRootMotion = false;
    }
}

あとは applyRootMotion を true に設定したいステートごとに取り付ければOK!

結果

キャラに付けてるスクリプトの変更は不要で、こんな感じに制御できます。
image.png
余談ですが、攻撃後など基本アクション(移動等)に遷移したいときは
上記のように遷移先を Exit に設定しとくとごちゃごちゃしづらいです。

他にもStateMachineBehaviourは応用の幅が広いので、ぜひ活用してくださいね。
何か改善点やトラブルがあったら教えてください!

追記

切り替えた瞬間に重力での加速がリセットされるような挙動は、
Animation側に BakeIntoPose(Y) を設定してあげれば解消できます。
※上下動のルートモーションが不要な場合

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