20201011のUnityに関する記事は4件です。

Unity ML-Agents で模倣学習やってみた

模倣学習やってみたです。コードはこちら

模倣学習とは?

人が操作した内容を基に学習することで、難しい内容でも学習を早く進められるというものです。

やったこと

エージェントが反時計回りにぐるぐるまわれるようにすることです。
bandicam-2020-10-10-20-24-48-884.gif

模倣内容は?

自分自身の操作で、反時計回りに5周ほど回してあげたものをデモとしています。

結果

水色:模倣あり
青色:模倣なし

学習グラフ

模倣ありのほうが早く学習していることがわかります。
bandicam 2020-10-11 20-19-25-482.jpg

模倣ありは効率的

Extrinsic も Curiosity も、両方とも模倣ありのほうが早く良い報酬が得られるようになっていることがわかります。
bandicam 2020-10-11 20-22-18-812.jpgbandicam 2020-10-11 20-22-13-970.jpg

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

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.FromCoroutineUniTaskで書くならどうしたらいいんだろう、というのが今回の話です。

書き方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について深く知りたいという方におすすめです。

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

Unity CommandBufferを用いて選択したオブジェクトにだけアウトラインをつける

概要

以下のgifのように、クリックしたオブジェクトにのみアウトラインを設定するようなものを、Unityの機能であるCommandBufferを利用して作成しました。
selective-outline.gif

プロジェクトのソースコードはこちら
https://github.com/Arihide/unity-selective-outline

解説

CommandBufferを用いると、レンダリングパイプラインの任意の箇所に、別の描画処理を挟み込むことができます。
今回はこの機能を用いて、以下の図のように、一部メッシュに対してだけレンダリング・輪郭抽出を行った後に合成を行いました。
selective-outline.png

次からは具体的なスクリプト部分について説明します。

スクリプト部分

まずは、CommandBufferを用いたスクリプト部分を見てみましょう。

SelectiveOutline.cs
using 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.shader
Shader "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つなので、重なりなどを考慮する必要がないからです。

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

UnityのおすすめAsset(自作)

完全に宣伝ですが便利なAsset2つ紹介します。

Simple Selection History

お気に入りなし無料版Simple Selection History Lite

Unityの選択履歴をとってくれるUtilityツールです。
中規模以上の開発で以前選択したAssetがどこにあるのか探すのに手間なことが多かったので作りました。
私としても気に入っているので開発現場に入ったときにはとりあえず勧めています。(無料版もありますし)

見た目はこんな感じ
image.png

機能

単純な選択履歴だけでは使っていてもあまりなので以下の機能があります。

  • ドラッグ&ドロップ
  • 拡張子フィルター
  • アセットを開く
  • ショートカット
  • お気に入り
  • お気に入りの表示名変更

使い方動画はこちら
(QiitaってYoutubeの動画貼れないのか・・・。)

設定

設定名 内容
Auto remove same file history 同じファイルの履歴の古い方を自動で削除します
Without Hierarchy object Hierarchy の選択は履歴に追加しなくなる
Scroll only history hisotryのみスクロールさせる
Update when selected history 履歴をクリックしたときにも履歴に追加します
(お気に入りの表示名を変更) ナンバリングなどのファイル名だと分からなので表示名を変更出来ます

設定動画はこちら

詳しいドキュメントのページはこちら

Utility Button

自分でボタンを作れるUtilityツールです。
開発中によく使うMenuItem・デバッグ機能や複数の手順をボタンに登録できるものです。
中規模以上の開発だとMenuがいっぱいになっていて選択の迷子になったり、決められた手順を何度もやるのが面倒だと思ったので作りました。

ボタンに作成するのはちょっと面倒ですが、自分だけのメニューを作れる感じです。
こちらは無料版はありません・・・。

見た目はこんな感じ(画面上に1列で配置したとき)
image.png

詳しいドキュメントはこちら

機能

結局何ができるかって言うと結構いろいろできる感じになってます。

  • MenuItemの実行
  • Assetの選択、開く
  • Log出力
  • 処理の遅延
  • Methodの実行
  • クラスメンバーへの値代入
  • 作成したボタンのショートカット登録

私がよく使うのは [Sceneを開く] [AssetBundleを作る] [よく使うインゲームのデバッグ機能] などボタンして効率化しています。

Asset Store Windowを開くのですが設定
image.png
複数のシーンを開く設定
image.png
Assetを開く設定
image.png

どうぞよろしくおねがいします。

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