- 投稿日:2020-10-11T20:26:50+09:00
Unity ML-Agents で模倣学習やってみた
模倣学習やってみたです。コードはこちら。
模倣学習とは?
人が操作した内容を基に学習することで、難しい内容でも学習を早く進められるというものです。
やったこと
エージェントが反時計回りにぐるぐるまわれるようにすることです。
模倣内容は?
自分自身の操作で、反時計回りに5周ほど回してあげたものをデモとしています。
結果
水色:模倣あり
青色:模倣なし学習グラフ
模倣ありは効率的
Extrinsic も Curiosity も、両方とも模倣ありのほうが早く良い報酬が得られるようになっていることがわかります。
- 投稿日:2020-10-11T03:11:44+09:00
UniRx/UniTask Observable.FromCoroutineをUniTaskで書く
まえおき:Observable.FromCoroutine
UniRxには
Observable.FromCoroutine
という、コルーチンからObservable
を生成する機能があります。
これを使うとそれなりに複雑なObservable
をコルーチンを用いて手続き的に記述可能です。UniRxというとどうしてもオペレータに意識をとらわれがちですが、実際はオペレータを使わずに実装した方が簡単に終わるケースもそれなりにあります。
そういった場面で活躍するのがこのObservable.FromCoroutine
です。例:長押しイベントを発行するObservable
たとえば長押しイベントを発行する
Observable
を実装してみます。オペレータで実装
/* * 1秒以上キーを長押すると true を発行 * 長押しをやめたタイミングで false を発行 */ var inputStream = Observable.EveryUpdate() .Select(_ => Input.GetKey(KeyCode.A)); var longPressObservable = inputStream .DistinctUntilChanged() .Throttle(TimeSpan.FromSeconds(1)) .Where(x => x) .Merge(inputStream.Where(x => !x).Skip(1)) .DistinctUntilChanged();コルーチンで実装
/* * 1秒以上キーを長押すると true を発行 * 長押しをやめたタイミングで false を発行 */ public IObservable<bool> CreateLongPressObservable() { return Observable.FromCoroutine<bool>(LongPressCoroutine); } private IEnumerator LongPressCoroutine(IObserver<bool> observer) { var pressTime = 0.0f; var lastValue = false; while (true) { if (Input.GetKey(KeyCode.A)) { pressTime += Time.deltaTime; if (pressTime >= 1.0f) { if (!lastValue) { observer.OnNext(true); lastValue = true; } } } else { pressTime = 0; if (lastValue) { observer.OnNext(false); lastValue = false; } } yield return null; } }オペレータ vs コルーチン
オペレータを用いた実装の方がコンパクトにまとまってはいます。
ですが初見でこれを読み解くのは難しく、UniRxにそれなりに慣れてないと若干難しいコードになっています。コルーチンの方はTHE・手続き型という昔ながらのコードになっています。
煩雑さはありますが、基本的なC#の文法さえわかっていれば読めるし編集できるというメリットがあります。正直、この程度の複雑さの例であればどっちで書いてもあまり変わらないとは思います。
複雑になってくるとコルーチンの方が書きやすい
ここに仕様が追加され「Bキーを押している間は長押しの判定間隔を0.5秒にしてほしい!」となったとします。
こうなってくるとオペレータの方ではかなり厳しいです。
UniRxは「後からObservable
の挙動を変える」というのが苦手です。そのためこの様な条件によって挙動が変わるObservable
は作りにくく、できたとしてもかなり複雑怪奇になるでしょう。一方、コルーチンの場合は数行修正する程度で対応が可能です。
仕様変更後private IEnumerator LongPressCoroutine(IObserver<bool> observer) { var pressTime = 0.0f; var lastValue = false; while (true) { // Bが押されている間だけしきい値を変える var thresholdTime = Input.GetKey(KeyCode.B) ? 0.5f : 1.0f; if (Input.GetKey(KeyCode.A)) { pressTime += Time.deltaTime; if (pressTime >= thresholdTime) { if (!lastValue) { observer.OnNext(true); lastValue = true; } } } else { pressTime = 0; if (lastValue) { observer.OnNext(false); lastValue = false; } } yield return null; } }宣言的な記述もいいですけど昔ながらの手続き的な記述方法も大事ですよ、という話ですね。
本題:コルーチンよりもUniTaskを使いたい
UniTaskを使うことで、コルーチンと同等の処理を
async/await
を使って記述できるようになります。じゃあ
Observable.FromCoroutine
もUniTask
で書くならどうしたらいいんだろう、というのが今回の話です。書き方1: Observable.Create + CancellationDisposableを使う
Observable.Create
を使えばIObserver<T>
が取得できるので、それを非同期メソッドに渡してしまえばOKです。
キャンセル対応はCancellationDisposable
を使います。public IObservable<bool> CreateLongPressObservable() { return Observable.Create<bool>(observer => { // CancellationDisposableは // IDisposableと連動するCancellationTokenを生成する var cd = new CancellationDisposable(); LongPressAsync(observer, cd.Token).Forget(); return cd; }); } private async UniTaskVoid LongPressAsync(IObserver<bool> observer, CancellationToken token) { var pressTime = 0.0f; var lastValue = false; while (true) { if (Input.GetKey(KeyCode.A)) { pressTime += Time.deltaTime; if (pressTime >= 1.0f) { if (!lastValue) { observer.OnNext(true); lastValue = true; } } } else { pressTime = 0; if (lastValue) { observer.OnNext(false); lastValue = false; } } await UniTask.Yield(PlayerLoopTiming.Update, token); } }より簡素に書く:UniTask.Voidを組み合わせる
UniTask.Void
を組み合わせるとメソッド内で処理を完結できます。
非同期メソッドの中身が小規模ならおすすめ。public IObservable<bool> CreateLongPressObservable() { return Observable.Create<bool>(observer => { var cd = new CancellationDisposable(); UniTask.Void(async ct => { var pressTime = 0.0f; var lastValue = false; while (true) { if (Input.GetKey(KeyCode.A)) { pressTime += Time.deltaTime; if (pressTime >= 1.0f) { if (!lastValue) { observer.OnNext(true); lastValue = true; } } } else { pressTime = 0; if (lastValue) { observer.OnNext(false); lastValue = false; } } await UniTask.Yield(PlayerLoopTiming.Update, ct); } }, cd.Token); return cd; }); }書き方2:UniTaskAsyncEnumerable -> Observable
UniTaskAsyncEnumerable
を使って記述してからObservable
に変換してしまう方法もあります。
UniTaskAsyncEnumerable.Create
が使えます。public IObservable<bool> CreateLongPressObservable() { return UniTaskAsyncEnumerable.Create<bool>(async (writer, ct) => { var pressTime = 0.0f; var lastValue = false; while (true) { if (Input.GetKey(KeyCode.A)) { pressTime += Time.deltaTime; if (pressTime >= 1.0f) { if (!lastValue) { writer.YieldAsync(true); lastValue = true; } } } else { pressTime = 0; if (lastValue) { writer.YieldAsync(false); lastValue = false; } } await UniTask.Yield(PlayerLoopTiming.Update, ct); } }).ToObservable(); }今回のケースでは最後に
Observable
へ変換していますが、場合によってはUniTaskAsyncEnumerable
のまま扱ったほうが効率的なケースも存在します。
Observable
を用いるべきか、UniTaskAsyncEnumerable
を用いるべきかはよく吟味しましょう。まとめ
Observable
の作り方はオペレータだけじゃない- 手続き的に書いたほうがトータルで楽な場合も存在する
- コルーチンを非同期メソッドに置き換えることも可能
UniTaskAsyncEnumerable
も便利なので使おう最後に
UniRx,UniTaskについて機能を紹介した「UniRx/UniTask完全理解(Amazon)」という本がまもなく発売されます。
電子書籍版も出ますのでUniRxやUniTaskについて深く知りたいという方におすすめです。
- 投稿日:2020-10-11T02:41:50+09:00
Unity CommandBufferを用いて選択したオブジェクトにだけアウトラインをつける
概要
以下のgifのように、クリックしたオブジェクトにのみアウトラインを設定するようなものを、Unityの機能であるCommandBufferを利用して作成しました。
プロジェクトのソースコードはこちら
https://github.com/Arihide/unity-selective-outline解説
CommandBufferを用いると、レンダリングパイプラインの任意の箇所に、別の描画処理を挟み込むことができます。
今回はこの機能を用いて、以下の図のように、一部メッシュに対してだけレンダリング・輪郭抽出を行った後に合成を行いました。
次からは具体的なスクリプト部分について説明します。
スクリプト部分
まずは、CommandBufferを用いたスクリプト部分を見てみましょう。
SelectiveOutline.csusing UnityEngine; using UnityEngine.Rendering; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class SelectiveOutline : MonoBehaviour { public Material emissionMaterial; public Material outlineMaterial; private new Camera camera; private CommandBuffer commandBuffer; [SerializeField] private Renderer targetRenderer = null; void OnEnable() { camera = GetComponent<Camera>(); commandBuffer = new CommandBuffer(); commandBuffer.name = "Selective Outline"; SetCommandBuffer(); // ImageEffects前(OnRenderImageが呼ばれる前)に適用 camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); } void OnDisable() { camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); } void SetCommandBuffer() { commandBuffer.Clear(); if (targetRenderer != null) { // レンダリング結果を格納するテクスチャ作成 var id = Shader.PropertyToID("_OutlineTex"); commandBuffer.GetTemporaryRT(id, -1, -1, 24, FilterMode.Bilinear); commandBuffer.SetRenderTarget(id); // アウトラインを表示させたいメッシュの描画 commandBuffer.ClearRenderTarget(false, true, Color.clear); commandBuffer.DrawRenderer(targetRenderer, emissionMaterial); // アウトラインを抽出して合成 commandBuffer.Blit(id, BuiltinRenderTextureType.CameraTarget, outlineMaterial); } } void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray = camera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit)) { targetRenderer = hit.transform.GetComponent<Renderer>(); SetCommandBuffer(); } } } }具体的な内容はソースコードのコメントを見ていただくとして、
大まかな処理の流れとしては、
OnEnable
関数内でCommandBufferオブジェクトを作成し、さらにSetCommandBuffer
関数内でどのような流れで描画するのかを設定しています。
その後、camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer);とすることによって、設定したCommandBufferをImageEffectsの直前(一通りメッシュの描画が完了したあと)に挟み込むようにします。
また
Update
関数内でオブジェクトがクリックされたのを検出したとき、そのオブジェクトにアウトラインをつけるように、CommandBufferの再設定を行っています。シェーダー部分
さて、処理の流れがわかったところで、次にアウトラインを抽出するシェーダーを見てみましょう。
Outline.shaderShader "Custom/Outline" { Properties { [HideInInspector]_MainTex ("Texture", 2D) = "white" {} _OutlineColor ("Outline Color", Color) = (1,1,1,1) _OutlineWidth ("Outline Width", Range(0, 10)) = 1 } SubShader { Tags { "Queue" = "Transparent" } Blend SrcAlpha OneMinusSrcAlpha Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; half2 _MainTex_TexelSize; half4 _OutlineColor; half _OutlineWidth; half4 frag (v2f_img i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); half2 destUV = _MainTex_TexelSize * _OutlineWidth; half left = tex2D(_MainTex, i.uv + half2(destUV.x, 0)).a; half right = tex2D(_MainTex, i.uv + half2(-destUV.x, 0)).a; half bottom = tex2D(_MainTex, i.uv + half2(0, destUV.y)).a; half top = tex2D(_MainTex, i.uv + half2(0, - destUV.y)).a; half topLeft = tex2D(_MainTex, i.uv + half2(destUV.x, destUV.y)).a; half topRight = tex2D(_MainTex, i.uv + half2(-destUV.x, destUV.y)).a; half bottomLeft = tex2D(_MainTex, i.uv + half2(destUV.x, -destUV.y)).a; half bottomRight = tex2D(_MainTex, i.uv + half2(-destUV.x, -destUV.y)).a; // あるピクセルの近傍が不透明であれば 1 half result = saturate(left + right + bottom + top + topLeft + topRight + bottomLeft + bottomRight); // 透過じゃないところはそのまま clip(0.99 - col.a); half4 outline = result * _OutlineColor; return outline; } ENDCG } } }よく見かけるアウトラインシェーダーは深度や法線による方法が多いですが、
今回はあるピクセルの近くが不透明だったら自分のピクセルも不透明とみなして輪郭を広げ、もともと不透明だったピクセル部分はくり抜く。という単純な方法で実装しました。
理由としては今回は描画対象が1つなので、重なりなどを考慮する必要がないからです。
- 投稿日:2020-10-11T01:19:09+09:00
UnityのおすすめAsset(自作)
完全に宣伝ですが便利なAsset2つ紹介します。
Simple Selection History
お気に入りなし無料版Simple Selection History Lite
Unityの選択履歴をとってくれるUtilityツールです。
中規模以上の開発で以前選択したAssetがどこにあるのか探すのに手間なことが多かったので作りました。
私としても気に入っているので開発現場に入ったときにはとりあえず勧めています。(無料版もありますし)機能
単純な選択履歴だけでは使っていてもあまりなので以下の機能があります。
- ドラッグ&ドロップ
- 拡張子フィルター
- アセットを開く
- ショートカット
- お気に入り
- お気に入りの表示名変更
使い方動画はこちら
(QiitaってYoutubeの動画貼れないのか・・・。)設定
設定名 内容 Auto remove same file history 同じファイルの履歴の古い方を自動で削除します Without Hierarchy object Hierarchy の選択は履歴に追加しなくなる Scroll only history hisotryのみスクロールさせる Update when selected history 履歴をクリックしたときにも履歴に追加します (お気に入りの表示名を変更) ナンバリングなどのファイル名だと分からなので表示名を変更出来ます 詳しいドキュメントのページはこちら
Utility Button
自分でボタンを作れるUtilityツールです。
開発中によく使うMenuItem・デバッグ機能や複数の手順をボタンに登録できるものです。
中規模以上の開発だとMenuがいっぱいになっていて選択の迷子になったり、決められた手順を何度もやるのが面倒だと思ったので作りました。ボタンに作成するのはちょっと面倒ですが、自分だけのメニューを作れる感じです。
こちらは無料版はありません・・・。詳しいドキュメントはこちら
機能
結局何ができるかって言うと結構いろいろできる感じになってます。
- MenuItemの実行
- Assetの選択、開く
- Log出力
- 処理の遅延
- Methodの実行
- クラスメンバーへの値代入
- 作成したボタンのショートカット登録
私がよく使うのは [Sceneを開く] [AssetBundleを作る] [よく使うインゲームのデバッグ機能] などボタンして効率化しています。
例
Asset Store Windowを開くのですが設定
複数のシーンを開く設定
Assetを開く設定
どうぞよろしくおねがいします。