simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:4画面テクスチャビューアの配置スクリプト

画像処理していると、複数枚のテクスチャを見比べたりしていきたいところなのですが
Android 環境にもっていくと、配置がずれていたりして困ったことになったので
簡単な配置スクリプトを作りました。

自分が再利用する目的で記事書きます。

f:id:simplestar_tech:20170402153656j:plain

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIQuadViewBehaviour : MonoBehaviour {

    public enum QuadLocation
    {
        LeftTop = 0,
        LeftBottom,
        RightTop,
        RightBottom
    }

    public QuadLocation MyLocation;

	// Use this for initialization
	void Start () {
        ResizeLocate();
    }
	
	// Update is called once per frame
	void Update () {
    }

    private void ResizeLocate()
    {
        int width = Screen.width / 2;
        int height = Screen.height / 2;

        Vector3 location = Vector3.zero;

        switch (MyLocation)
        {
            case QuadLocation.LeftTop:
                location.x = -width / 2;
                location.y = height / 2;
                break;
            case QuadLocation.LeftBottom:
                location.x = -width / 2;
                location.y = -height / 2;
                break;
            case QuadLocation.RightTop:
                location.x = width / 2;
                location.y = height / 2;
                break;
            case QuadLocation.RightBottom:
                location.x = width / 2;
                location.y = -height / 2;
                break;
            default:
                break;
        }

        RectTransform rc = GetComponent<RectTransform>();

        rc.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
        rc.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
        rc.localPosition = location;
    }
}

UI の RawImage にこのスクリプトコンポーネントを付けると、起動時に画面4分割の配置に移動します。
Anchor Presets は center と middle の組み合わせに限ります。

PC でテストして Android で実行するとビューが壊れているなんていう問題は、一応これで回避
テクスチャの解像度やアスペクト比まで考えて、賢く配置してくれと言いたい文句もありますが、そこはテクスチャをセットする側が調整してあげて…

Unity:深度画像の取得

オブジェクトのエッジを強調したり、モデルベースの画像処理を行いたいときなどは、見ているシーンの深度画像が必要になります。(これは本当)

OpenCV に現在フレームの深度画像を渡したいときは float 配列としてカメラからのZ距離が得られると文句はないのです。
この記事ではその float 配列の取得方法を動作確認込みで示します。

f:id:simplestar_tech:20170402120750j:plain

Unity で深度画像を得る方法をいくつか存じておりますが、私が知る中で最速な方法を示したいと思います。

前回、前々回と触ってきた Compute Shader を使います。(以下の方法が具体的にイメージできない人は読み返してね)

Compute Shader を新規作成したら、次のコードを記述します。

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

float n_f;
float f;
Texture2D<float> _zBuffer;
RWTexture2D<float> _cameraZ;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
#if SHADER_API_GLES3
	// for Android
	_cameraZ[id.xy] = 1.0f / (n_f * (1.0f - _zBuffer[id.xy]) + f);
#else
	// for Windows
	_cameraZ[id.xy] = 1.0f / (n_f * _zBuffer[id.xy] + f);
#endif
}

単なるシェーダーではなく Compute Shader であるため UNITY_REVERSED_Z による判定が行えません。
Android では UNITY_REVERSED_Z ではなかったので泣く泣く SHADER_API_GLES3 でコードを変更しています。

この Compute Shader を使うスクリプトは次の通り

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class VirtualCameraBehaviour : MonoBehaviour {

    private int width;
    private int height;

    Camera _virtualCamera = null;

    RenderTexture _color = null;
    RenderTexture _zBuffer = null;

    public RenderTexture _cameraZ = null;

    public ComputeShader Depth;
    public GameObject uiResult0;
    public GameObject uiResult1;
    public GameObject uiResult2;

    void Start () {

        width = Screen.width / 2;
        height = Screen.height / 2;

        _virtualCamera = GetComponent<Camera>();

        _color = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
        _zBuffer = new RenderTexture(width, height, 32, RenderTextureFormat.Depth);

        _virtualCamera.SetTargetBuffers(_color.colorBuffer, _zBuffer.depthBuffer);

        _cameraZ = new RenderTexture(width, height, 0, RenderTextureFormat.RFloat);
        _cameraZ.enableRandomWrite = true;
        _cameraZ.Create();

        float n_inv = 1.0f / _virtualCamera.nearClipPlane;
        float f_inv = 1.0f / _virtualCamera.farClipPlane;
        Depth.SetFloat("n_f", n_inv - f_inv);
        Depth.SetFloat("f", f_inv);
        Depth.SetTexture(0, "_zBuffer", _zBuffer);
        Depth.SetTexture(0, "_cameraZ", _cameraZ);

        uiResult0.GetComponent<UnityEngine.UI.RawImage>().texture = _color;
        uiResult1.GetComponent<UnityEngine.UI.RawImage>().texture = _cameraZ;
        uiResult2.GetComponent<UnityEngine.UI.RawImage>().texture = _zBuffer;
    }
	
	void Update () {
		
	}

    private void OnPostRender()
    {
        Depth.Dispatch(0, _zBuffer.width / 8, _zBuffer.height / 8, 1);
    }
}

オフスクリーンレンダリングで、解像度を指定して VirtualCamera 画像を作り、そのカラー画像や深度画像を float 32bit 深度 1チャンネル画像として取得しています。

CPU 側で float 配列を手に入れたい場合は次の一工夫を加えて完了です。

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

float n_f;
float f;
int width;
Texture2D<float> _zBuffer;
RWTexture2D<float> _cameraZ;

// for float Array
RWStructuredBuffer<float> _zArray;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
#if SHADER_API_GLES3 // insted of UNITY_REVERSED_Z
	// for Android
	_cameraZ[id.xy] = 1.0f / (n_f * (1.0f - _zBuffer[id.xy]) + f);
#else
	// for Windows
	_cameraZ[id.xy] = 1.0f / (n_f * _zBuffer[id.xy] + f);
#endif

	_zArray[id.x + id.y * width] = _cameraZ[id.xy];
}

以下の対応で CPU 側の float 配列に深度画像の画素値が得られることを確認しました。
これで OpenCV にこのデータを使ってもらうことにより、さまざまなモデルベース処理が行えるという夢が広がります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Camera))]
public class VirtualCameraBehaviour : MonoBehaviour {

    private int width;
    private int height;

    Camera _virtualCamera = null;

    RenderTexture _color = null;
    RenderTexture _zBuffer = null;

    public RenderTexture _cameraZ = null;

    public ComputeShader Depth;
    ComputeBuffer _zArray;
    float[] _zArrayData;

    public GameObject uiResult0;
    public GameObject uiResult1;
    public GameObject uiResult2;

    void Start () {

        width = Screen.width / 2;
        height = Screen.height / 2;

        _virtualCamera = GetComponent<Camera>();

        _color = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
        _zBuffer = new RenderTexture(width, height, 32, RenderTextureFormat.Depth);

        _virtualCamera.SetTargetBuffers(_color.colorBuffer, _zBuffer.depthBuffer);

        _cameraZ = new RenderTexture(width, height, 0, RenderTextureFormat.RFloat);
        _cameraZ.enableRandomWrite = true;
        _cameraZ.Create();

        float n_inv = 1.0f / _virtualCamera.nearClipPlane;
        float f_inv = 1.0f / _virtualCamera.farClipPlane;
        Depth.SetFloat("n_f", n_inv - f_inv);
        Depth.SetFloat("f", f_inv);
        Depth.SetTexture(0, "_zBuffer", _zBuffer);
        Depth.SetTexture(0, "_cameraZ", _cameraZ);

        _zArrayData = new float[width * height];
        _zArray = new ComputeBuffer(_zArrayData.Length, sizeof(float));
        Depth.SetBuffer(0, "_zArray", _zArray);
        Depth.SetInt("width", width);

        uiResult0.GetComponent<UnityEngine.UI.RawImage>().texture = _color;
        uiResult1.GetComponent<UnityEngine.UI.RawImage>().texture = _cameraZ;
        uiResult2.GetComponent<UnityEngine.UI.RawImage>().texture = _zBuffer;
    }
	
	void Update () {
		
	}

    private void OnPostRender()
    {
        Depth.Dispatch(0, _zBuffer.width / 8, _zBuffer.height / 8, 1);
        _zArray.GetData(_zArrayData);

        float av = 0;
        for (int i = 0; i < _zArrayData.Length; i++)
        {
            av += _zArrayData[i];
        }
        av /= _zArrayData.Length;
        Debug.Log("average z = " + av.ToString("0.0000"));
    }
}

Unity:Compute Shaderでカメラ画像処理する時に最初に書くコード

ひとつ前の記事で Compute Shader を Android で実行するサンプルを載せましたので、今回はその Compute Shader を使ってカメラ画像をリアルタイムに処理します。

f:id:simplestar_tech:20170326232644j:plain

できました!
これからリアルタイムカメラ画像処理を Compute Shader で行う際は、以下のコードをコピペして使っていこうと思います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DeviceCameraBehaviour : MonoBehaviour {

    // Histogram Gather
    public GameObject resultCube;
    public ComputeShader RGBA2GRAY;
    public Texture2D Source;
    RenderTexture _result;
    int _kernelIndex = 0;
    ComputeBuffer _HistogramBuffer = null;
    uint[] _emptyBuffer = new uint[256];
    uint[] _histogramBuffer = new uint[256];

    // Device Camera
    string _activeDeviceName = "";
    WebCamTexture _activeTexture = null;

    // Use this for initialization
    void Start () {

        _HistogramBuffer = new ComputeBuffer(256, sizeof(uint));
        for (int i = 0; i < _emptyBuffer.Length; i++)
        {
            _emptyBuffer[i] = 0;
        }
        _HistogramBuffer.SetData(_emptyBuffer);

        _kernelIndex = RGBA2GRAY.FindKernel("KHistogramGather");
        RGBA2GRAY.SetBuffer(_kernelIndex, "_Histogram", _HistogramBuffer);
    }
	
	// Update is called once per frame
	void Update () {

        if (null != _activeTexture && _activeTexture.isPlaying && _activeTexture.didUpdateThisFrame)
        {
            if (null == _result)
            {
                _result = new RenderTexture(_activeTexture.width, _activeTexture.height, 0, RenderTextureFormat.ARGB32);
                _result.enableRandomWrite = true;
                _result.Create();
                RGBA2GRAY.SetTexture(_kernelIndex, "_Source", _activeTexture);
                RGBA2GRAY.SetTexture(_kernelIndex, "_Result", _result);
                RGBA2GRAY.SetVector("_SourceSize", new Vector2(_activeTexture.width, _activeTexture.height));
                _HistogramBuffer.SetData(_emptyBuffer);
                resultCube.GetComponent<Renderer>().material.mainTexture = _result;

                resultCube.transform.localScale = new Vector3((_activeTexture.width/(float)_activeTexture.height), 1, 1);
            }
            RGBA2GRAY.Dispatch(_kernelIndex, Mathf.CeilToInt(_activeTexture.width / 32f), Mathf.CeilToInt(_activeTexture.height / 32f), 1);
        }

        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Time = " + Time.time.ToString("000.0000"));
            PlayDeviceCamera();

        }
        for (int i = 0; i < Input.touchCount; ++i)
        {
            if (Input.GetTouch(i).phase == TouchPhase.Began)
            {

                // Construct a ray from the current touch coordinates
                Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(i).position);
                // Create a particle if hit
                if (Physics.Raycast(ray))
                {
                    Debug.Log("Time = " + Time.time.ToString("000.0000"));
                    PlayDeviceCamera();
                }
            }
        }
    }

    private void PlayDeviceCamera()
    {
        WebCamDevice[] devices = WebCamTexture.devices;
        for (int i = 0; i < devices.Length; i++)
        {
            if (0 != string.Compare(_activeDeviceName, devices[i].name))
            {
                if (null != _activeTexture)
                {
                    _activeTexture.Stop();
                }
                _activeDeviceName = devices[i].name;
                _activeTexture = new WebCamTexture(devices[i].name);
                _activeTexture.Play();
                RGBA2GRAY.SetTexture(_kernelIndex, "_Source", _activeTexture);
                _result = null;
                Debug.Log(devices[i].name);
                break;
            }
        }
    }
}

任意の解像度を指定するとか、細かいことをこの後入れていく感じですね。
同じコードで Android の方でも、リアルタイムカメラ画像処理できることを確認しました。

Unity:AndroidでComputeShaderを使ってみました

画像処理では、並列計算による高速化が求められます。(常に…)
Compute Shader を使った並列計算による高速化は実装が簡単で効果抜群なのですが、DirectX 依存なので(と思い込んでいた私は) PC 上での動作に限られていると思っていました。

しかし、ふと Unity の Compute Shader って Android 用にビルドした場合も動作するのかな?
だとしたら、Unity すばらしいなって思いまして、さっそく試してみました。

左は入力のカラー画像、右は出力のグレースケール画像(…イラストは私が描きました)

f:id:simplestar_tech:20170326171134j:plain

できた!Unityすばらしい!

使った Compute Shader のコードはこちら↓

// file head
RWStructuredBuffer<uint> _Histogram;
RWTexture2D<float4> _Result;

// Gathering pass
Texture2D<float4> _Source;

uint2 _SourceSize;

// groupshared uint gs_histogram[256]; // group shared memory can be used in a Windows PC, but it could not be used in an Android platform. why?

#pragma kernel KHistogramGather
[numthreads(32, 32, 1)]
void KHistogramGather(uint3 id : SV_DispatchThreadID, uint3 _group_thread_id : SV_GroupThreadID)
{
	const uint thread_id = _group_thread_id.y * 32 + _group_thread_id.x;

	//if (thread_id == 0)
	//{
	//	for (int i = 0; i < 256; i++)
	//	{
	//		gs_histogram[i] = 0;
	//	}
	//}

	// GroupMemoryBarrierWithGroupSync();

	if (id.x < _SourceSize.x && id.y < _SourceSize.y)
	{
		float3 color = saturate(_Source[id.xy].xyz);

		// Convert color to grayscale
		float luminance = dot(color.rgb, float3(0.2125, 0.7154, 0.0721));
		uint idx_l = (uint)round(luminance * 255.0);

		// InterlockedAdd(gs_histogram[idx_l], 1);
		InterlockedAdd(_Histogram[idx_l], 1); // it is an alternative method for using a group shared memory
		_Result[id.xy] = float4(luminance, luminance, luminance, 1);
	}

	// GroupMemoryBarrierWithGroupSync();

	//if (thread_id == 0)
	//{
	//	for (int i = 0; i < 256; i++)
	//	{
	//		InterlockedAdd(_Histogram[i], gs_histogram[i]);
	//	}
	//}
}

単にグレースケール化するだけじゃなくて、輝度ヒストグラムまで構築するスグレモノなのですが
残念なことに groupshared メモリを使った高速化がAndroid上だけ、できないことが分かったのでコメントアウトしています。

シェーダーの利用はスクリプト側で行いますので、利用スクリプトの方も以下に載せます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DeviceCameraBehaviour : MonoBehaviour {

    public GameObject resultCube;
    public ComputeShader RGBA2GRAY;
    public Texture2D Source;
    public RenderTexture Result;

    int _kernelIndex = 0;
    ComputeBuffer _HistogramBuffer = null;
    uint[] _emptyBuffer = new uint[256];
    uint[] _histogramBuffer = new uint[256];

    // Use this for initialization
    void Start () {

        Result = new RenderTexture(Source.width, Source.height, 0, RenderTextureFormat.ARGB32);
        Result.enableRandomWrite = true;
        Result.Create();

        _HistogramBuffer = new ComputeBuffer(256, sizeof(uint));
        for (int i = 0; i < _emptyBuffer.Length; i++)
        {
            _emptyBuffer[i] = 0;
        }
        _HistogramBuffer.SetData(_emptyBuffer);

        _kernelIndex = RGBA2GRAY.FindKernel("KHistogramGather");
        RGBA2GRAY.SetBuffer(_kernelIndex, "_Histogram", _HistogramBuffer);
    }
	
	// Update is called once per frame
	void Update () {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            DoComputeShader();

            Debug.Log("Time = " + Time.time.ToString("000.0000"));
            WebCamDevice[] devices = WebCamTexture.devices;
            for (int i = 0; i < devices.Length; i++)
                Debug.Log(devices[i].name);
        }
        for (int i = 0; i < Input.touchCount; ++i)
        {
            if (Input.GetTouch(i).phase == TouchPhase.Began)
            {

                // Construct a ray from the current touch coordinates
                Ray ray = Camera.main.ScreenPointToRay(Input.GetTouch(i).position);
                // Create a particle if hit
                if (Physics.Raycast(ray))
                {
                    DoComputeShader();

                    Debug.Log("Time = " + Time.time.ToString("000.0000"));
                    WebCamDevice[] devices = WebCamTexture.devices;
                    for (int j = 0; j < devices.Length; j++)
                        Debug.Log(devices[j].name);
                }
            }
        }
    }

    private void DoComputeShader()
    {
        RGBA2GRAY.SetTexture(_kernelIndex, "_Source", Source);
        RGBA2GRAY.SetTexture(_kernelIndex, "_Result", Result);
        RGBA2GRAY.SetVector("_SourceSize", new Vector2(Source.width, Source.height));
        _HistogramBuffer.SetData(_emptyBuffer);
        RGBA2GRAY.Dispatch(_kernelIndex, Mathf.CeilToInt(Source.width / 32f), Mathf.CeilToInt(Source.height / 32f), 1);
        _HistogramBuffer.GetData(_histogramBuffer);
        uint total = 0;
        for (int i = 0; i < _histogramBuffer.Length; i++)
        {
            total += _histogramBuffer[i];
        }
        Debug.Log("histogram total = " + total);
        resultCube.GetComponent<Renderer>().material.mainTexture = Result;
    }
}

簡単にコードを解説してみますと

RGBA2GRAY というのが、Compute Shader で、先ほど示したコードで作ったシェーダーを Unity のエディタから紐づけます。
あとは、画像データを Source テクスチャとして用意して渡したら Disptach にて 32 分の 1 の幅と高さのスレッドグループ数を指定し実行しています。
なぜ 32 分の 1 にしなければいけないかというと、Compute Shader 側で[numthreads(32, 32, 1)]と、32 x 32のタイル領域ごとに処理をすると宣言しているからです。
Dispach で縦横のタイル数を指定してあげることで全画素についての処理が完成するというイメージを持っていただけると、理解しやすいかなって思います。

最後に Andorid のビルド設定ですが、以下のようにして、端末は2017年1月に購入した Phab 2 Pro で動作することを確認しました。(自身の携帯である Xperia Z3 では Compute Shader は動かなかった…)

f:id:simplestar_tech:20170326174354j:plain

今回の記事は以下のページを参考に作成しました。

まず Compute Shader を一度も触ったことがない人は、こちらの記事を参考にするとサクッとPC上で動作確認までできるようになると思います。
[Unity] UnityでComputeShaderを使う解説をしているページを訳してみた その2 - Qiita

次にテクスチャを入力に、Compute Shader で何かしら画素値を処理してテクスチャを出力する場合については次の記事を参考にします。
DirectCompute tutorial for Unity 3: Textures | Cheney Shen

ヒストグラムの計算については、次のプロジェクトの、このプルリクエストを参考にしました。(ヒストグラムなのに並列計算で高速化!?できないと思っていたので驚きましたよ、考えた人グッジョブです!)
Unity-Technologies / cinematic-image-effects / Pull request #31: [tcg] Histogram compute shader optimizations (~4x) — Bitbucket

Unity:uGUIにDebug.Logの内容を表示する方法1

UnityでDebug.Logした内容を、uGUIのUI画面でゲーム実行中に確認したい場合があります。
今回はその要望に応える形で次のようにログが流れる仕組みを作ったので、今後自分が再利用するために公開します。

f:id:simplestar_tech:20170319195427j:plain

Debug.Log をハンドリングするコードは次の通り

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

public class UILogBehaviour : MonoBehaviour {

    public Text TextPrefab;
    private RectTransform _myPanel;

	// Use this for initialization
	void Start () {
        _myPanel = GetComponent<RectTransform>();
    }
	
	// Update is called once per frame
	void Update () {
		
	}

    void OnEnable()
    {
        Application.logMessageReceived += Log;
    }

    void OnDisable()
    {
        Application.logMessageReceived -= Log;
    }

    public void Log(string logString, string stackTrace, LogType type)
    {
        Text logLine = Instantiate(TextPrefab, Vector3.zero, Quaternion.identity, _myPanel);
        logLine.name = "LogLine";
        logLine.text = logString;

        if (20 < _myPanel.transform.childCount)
        {
            Transform child = _myPanel.GetChild(1);
            GameObject.Destroy(child.gameObject);
        }
    }
}

使い方:このスクリプトを VerticalLayoutGroup コンポーネントを付けた Panel に追加して、最初の子要素の Text を TextPrefab に渡します。

すると 20 件以上ログが流れたら、古いログのラインから順番に消えていきます。
テスト用に Space キーを押したらログが流れる仕組みを次のように書いて、テストしました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DeviceCameraBehaviour : MonoBehaviour {

	// Use this for initialization
	void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Time = " + Time.time.ToString("000.0000"));
        }
	}
}

Android などの実機で、デバイスの設定がどうなっているかなどを簡易にに確認する方法として使えると思い、作ってみました。
以上です。

対戦ゲームで一喜一憂するAI

強化学習でまるばつゲーム(3目ならべ)を作ったことがありましたが、ふと相手の手や自分の手に関して一喜一憂するAIが作れるのではないかと思ったわけです。
作ってみます。

AIには最近扱いに慣れてきたSDユニティちゃんを使わせてもらおうと思います。
感情表現としては、用意されている表情でやってみますか。

smile2, confuse, sad, scold, strain, surprise, damaged, relux の計 8 種類の感情表現とします。

状況に応じて、どの感情を表すかは後で決めることにしますか。
まずは対戦ゲームの方を完成させます。

簡単なユースケース駆動開発をしてみます。
まずは「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。
最初に「先攻後攻決めアニメ」が走ります。
自分の番になると、「青い Your Turn」のボードが一瞬出てきて消えます。
「盤面」の「マス」にホバーすると「半透明の〇 or ×マーク」がマス内に表示されます。
ホバーするマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
配置したいマスをクリックすると、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
相手の番になると「赤い Enemy Turn」のボードが一瞬でてきて消えます。
「SDユニティちゃん」が感情表現しながら、マウスをホバーして「半透明の〇 or ×マーク」がマス内に表示され、そのマスごとに「感情表現」を変えます。
意思決定をしたら、SDユニティちゃんは、空いているマスに配置、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
「ゲームが終了、勝敗」が決まると、「SDユニティちゃん」が「感情表現」を変化させます。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。

ここで出てきたオブジェクトを列挙します。
・リトライ(開始)ボタン
・先攻後攻決めアニメ
・赤い Enemy Turn ボード、青い Your Turn ボード
・盤面/マス
・SDユニティちゃん
・感情
・配置音
・半透明、不透明の〇×マーク
・ゲーム結果、勝敗
まずはこれらのアセットを準備します。

…そろいました。
f:id:simplestar_tech:20170205202442j:plain

ではユースケースを実装してみましょう。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直し
「先攻後攻決めアニメ」が走る
自分・相手の番になると、「青い・赤い Turn」のボードが一瞬出てきて消える
「盤面」の「マス」にマウスがホバーすると「半透明の〇 or ×マーク」がマス内に表示
ホバーするマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
配置したいマスをクリックすると、「配置音」と同時に「不透明の〇×マーク」がマス内に配置される。
相手の番になると「赤い Enemy Turn」のボードが一瞬でてきて消える。

f:id:simplestar_tech:20170206004929j:plain

とりあえず、今ここまで実装を進めてます。

意思決定をしたら、SDユニティちゃんは、空いているマスに配置、「配置音」と同時に「不透明の〇×マーク」がマス内に配置されます。
配置されたマス目ごとに「SDユニティちゃん」が「感情表現」を変化させます。
「ゲームが終了、勝敗」が決まると、「SDユニティちゃん」が「感情表現」を変化させます。
「リトライ(開始)ボタン」を押すとゲームが最初からやり直しとなります。

f:id:simplestar_tech:20170227090030j:plain

をやりました。

次のだけ入っていない
「SDユニティちゃん」が感情表現しながら、マウスをホバーして「半透明の〇 or ×マーク」がマス内に表示され、そのマスごとに「感情表現」を変えます。

時間が空いてしまって、熱量が足りない…
気が向いたら、入れようと思います。

一喜一憂するAIは、わかっていても対戦していてなかなか楽しいことがわかりました。

f:id:simplestar_tech:20170227090243j:plain

ユニティちゃんライセンス

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

Unity:mecanimのイロハ

SD Toon シェーダーの Unityちゃんで、ステート遷移によってアニメーションが変化する仕組み mecanim を勉強してみようと思います。
ほんとに今更ですが、基本的なところを触ってみます。

f:id:simplestar_tech:20170205134929j:plain

ボーンの入ったモデルと、そのボーンを動かすアニメーションクリップを用意します。
具体的にはモーションを設定した FBX ファイルをどこからか持ってくるという作業です。

今回は SD Toon Unity ちゃんとしました。
入手場所はこちら
unity-chan.com

手順をわかりやすく、まっさらなシーンに Unity ちゃんのモデルを配置してみます。

f:id:simplestar_tech:20170205135922j:plain

メッシュにはディフォルトのマテリアルが割り当てられているので Toon フォルダにある body 用、face 用 hair 用などをそれぞれのメッシュに割り当てます。
加えて、ディレクショナルライトを3つつくり、それぞれ別のレイヤーだけに影を落とすようにし
body, face, hair, head のレイヤーを変更し
Lighting にて Ambient を Skybox から Color White に設定します。
するとこんな見た目に変化します。

f:id:simplestar_tech:20170205142140j:plain

Model をそのまま配置しただけですが、ディフォルトで Animator と Avatar まで割り当たっていました。
これは便利なのでそのまま使いましょう。

次に AnimatorController を作ります。

最初はブレンドツリーというステートを一つ作ってみます。
このステートをダブルクリックすると、次のようなブレンドに関するUIが出てきます。(すでに別途 Walk と Run のモーションをブレンドするように追加した状態です。最初は何もないよ)

f:id:simplestar_tech:20170205145425j:plain

Blend パラメータを調整すれば、二つのモーションをその割合でブレンドするようになります。
そのほかのパラメータ設定は調整次第ですね。ここまで基本的なことができれば、モーションブレンドの基本は OK だと思います。

次に、アニメーションイベントをやってみます。

アニメーションクリップを見てみると、Walk に関しては次のようにアニメーションイベントが設定されています。

f:id:simplestar_tech:20170205150200j:plain

Animator が設定されている階層に、スクリプトコンポーネントを配置して、ここにある Function 名の関数を用意してみます。
すると、アニメーションが開始されたタイミングでこの関数が呼ばれます。

要は、モーションのあるタイミングで表情や音、エフェクトなどを発生させたいときなどに、これらのイベントをモーションに仕込んでおくと便利なことになりそうです。
何処にイベントを仕込んだのか、一覧表示できるとうれしいですが、そういうところ気が利かない機能かもしれません。(どこからも呼ばれないはずの関数が呼ばれてしまうというバグが残りそう…)

public class UnityChanBehaviour : MonoBehaviour {
    public AnimationClip[] animations;
    // Use this for initialization
    void Start () {
		
	}
	
	// Update is called once per frame
	void Update () {
		
	}

    public void OnCallChangeFace(string str)
    {
        int ichecked = 0;
        foreach (var animation in animations)
        {
            if (str == animation.name)
            {
                ChangeFace(str);
                break;
            }
            else if (ichecked <= animations.Length)
            {
                ichecked++;
            }
            else
            {
                //str指定が間違っている時にはデフォルトで
                str = "default@unitychan";
                ChangeFace(str);
            }
        }
    }

    void ChangeFace(string str)
    {
        //isKeepFace = true;
        //current = 1;
        //anim.CrossFade(str, 0);
    }
}

次に基本的な機能の、ブレンドマスクを見てみます。
例えば、先ほどのアニメーションイベントにて呼ばれた関数で、顔の表情だけを別のアニメーションにしてみたいと思います。

ブレンドマスクにはこんな感じで、faceだけ適用する旨を設定します。
f:id:simplestar_tech:20170205153913j:plain
あとは Face という、もう一つのレイヤーを AnimationController に追加して、そこに表情のステートを追加します。遷移をいちいち書かないのがポイント。
f:id:simplestar_tech:20170205154006j:plain

アニメーションイベントで呼ばれる関数にて、CrossFade 関数を呼べば、遷移をかかずとも、そのステートに遷移します。

    public void OnCallChangeFace(string str)
    {
        int ichecked = 0;
        foreach (var animation in animations)
        {
            if (str == animation.name)
            {
                ChangeFace(str);
                break;
            }
            else if (ichecked <= animations.Length)
            {
                ichecked++;
            }
            else
            {
                //str指定が間違っている時にはデフォルトで
                str = "default@unitychan";
                ChangeFace(str);
            }
        }
    }

    void ChangeFace(string str)
    {
        anim.CrossFade(str, 0.4f);
    }

歩いているときに飛んでくるアニメーションイベントでスマイルし
f:id:simplestar_tech:20170205154159j:plain

走っているときに飛んでくるアニメーションイベントで驚きます。
f:id:simplestar_tech:20170205154242j:plain

gif アニメをとるとこんなイメージです。

f:id:simplestar_tech:20170205155112g:plain

mecanimの基本的な機能はこんなところでしょうか。
もっと便利な機能を見つけましたら、またどこかで書こうと思います。

ユニティちゃんライセンス

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