e.blog

主にUnity/UE周りのことについてまとめていきます

フリーハンドで描いた図形をポリゴン分割する

概要

フリーハンドで平面に描いた形に図形を形成するのをやりたかったので、頂点群からポリゴンを形成する処理について書きたいと思います。

こんな感じで、適当に打った点からポリゴンを形成します↓

点群からポリゴンを形成する

まず必要になるのが、点群からポリゴンを形成する処理です。

実装についてはこちらの記事(Javaゲーム制作記 任意多角形の三角形分割)を参考にさせていただきました。

ちなみに、記事中に、さらに元となった記事へのリンクが書いてありましたがリンク切れしてました。
おそらく元となった記事はこちらだと思います↓

sonson.jp

任意の点群からポリゴンを形成する

点群からポリゴンを形成するには、当然ですが点群が囲む任意多角形をポリゴンとして三角形に分割していく作業が必要になります。

大まかに流れを書くと以下のようになります。

  1. 点群を得る
  2. 点群の中で、任意の点(※1)から一番遠い点を見つける
  3. (2)で見つかった点とその両隣の点で三角形を作る
  4. このとき、(2)の点と両隣の点から成る線が作る角度が180度を超えていないことを確認する(※2)
  5. (3)で形成した三角形の中に、点郡の他の点が含まれていないことを確認する
  6. (5)で内包している点がなかったら、それを分割された三角形として採用し、(2)で見つかった点を点群リストから除外する
  7. もし(5)の工程で内包する点が見つかった場合は三角形が構成できないので、ひとつ隣の点を採用点に変更し、もう一度(5)の処理を行う。その際、確認した三角形の向きを保持しておく(※3)
  8. (5)の処理を行い、見つかった三角形と、(7)で保持していた三角形の向きをチェックし、異なった方向だった場合はまた隣の点に移動して(5)の処理を繰り返す
  9. 以後、点群が残り3点(三角形が構成できる最後の点群)になるまで繰り返す

※1 ... 任意の点なのでどこでも構いません。原点などが採用しやすいでしょう。
※2 ... もし超えている場合、見つけた点よりも遠い点が存在するため判定がおかしい。
※3 ... 向きをチェックする理由は、多角形の点の構成によっては外側の三角形が見つかる可能性があるため(図解で後述します)

以上の手順を繰り返すことで、点群を複数の三角形に分割し、冒頭の画像のように任意の多角形をポリゴンに分解することができるようになります。

・・・と、文章だとなんのこっちゃ、だと思うのでまずは図解してみます。

f:id:edo_m18:20180324231129p:plain f:id:edo_m18:20180324231123p:plain f:id:edo_m18:20180324231218p:plain f:id:edo_m18:20180324231222p:plain f:id:edo_m18:20180324231321p:plain f:id:edo_m18:20180324231324p:plain f:id:edo_m18:20180324231327p:plain f:id:edo_m18:20180324231330p:plain f:id:edo_m18:20180324231337p:plain f:id:edo_m18:20180324231340p:plain f:id:edo_m18:20180324231346p:plain f:id:edo_m18:20180324231349p:plain
作った三角形の中に別の点が含まれてしまっている f:id:edo_m18:20180324231353p:plain
三角形の向きは採用点から次の点を辺1、前の点を辺2として、その外積を使って求める f:id:edo_m18:20180324231356p:plain f:id:edo_m18:20180324231402p:plain f:id:edo_m18:20180324231405p:plain f:id:edo_m18:20180324231408p:plain

以上のような感じで、順々に三角形に分解していき、最後の3頂点になるまでそれを繰り返す、という方法です。
仕組み自体はとてもシンプルですね。

コードで見てみる

今回実装したコードも合わせて載せておきます。
(図解したことをコードにしているだけなので、詳細は割愛します)

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

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<Vector3> _leftVertices = new List<Vector3>();
    private List<Vector3> _triangles = new List<Vector3>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _leftVertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _leftVertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _leftVertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear vertices and triangles.
    /// </summary>
    private void ClearMesh()
    {
        _leftVertices.Clear();
        _triangles.Clear();
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        ClearMesh();

        _leftVertices.AddRange(vertices);

        while (_leftVertices.Count > 3)
        {
            DetecteTriangle();
        }

        _triangles.AddRange(_leftVertices);

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _triangles.ToArray();

        int[] indices = new int[_triangles.Count];
        for (int i = 0; i < indices.Length; i ++)
        {
            indices[i] = i;
        }

        mesh.triangles = indices;
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle direction.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(a);
        _triangles.Add(b);
        _triangles.Add(c);

        _leftVertices.RemoveAt(_curIndex);
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            // skip if index in detected three points.
            if (i == _curIndex || i == _nextIndex || i == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_leftVertices[i]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint);
        Vector3 edge2 = (PreviousPoiont - CurrentPoint);

        return Vector3.Cross(edge1, edge2).normalized;
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = (_curIndex + 1) % _leftVertices.Count;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = _curIndex - 1 >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }

    /// <summary>
    /// Find far point from origin.
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            float dist = Vector3.Distance(Vector3.zero, _leftVertices[i]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = i;
            }
        }

        _curIndex = farIndex;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = (_curIndex - 1) >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }
}

[2018.05.02追記]

上記コードではポリゴン用の頂点を複製してポリゴン数(x3)と同じだけの頂点を生成していましたが、インデックスを割り当てるように修正したのでそちらのコードも載せておきます。

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

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<int> _triangles = new List<int>();
    private List<Vector3> _vertices = new List<Vector3>();
    private Dictionary<int, bool> _verticesBuffer = new Dictionary<int, bool>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _vertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _vertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _vertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear buffers.
    /// </summary>
    private void Clear()
    {
        _vertices.Clear();
        _verticesBuffer.Clear();
        _triangles.Clear();
    }

    private void Initialize(List<Vector3> vertices)
    {
        Clear();

        // 設定された頂点を保持しておく
        _vertices.AddRange(vertices);

        // 全頂点のインデックスを保持、使用済みフラグをfalseで初期化
        for (int i = 0; i < vertices.Count; i++)
        {
            _verticesBuffer.Add(i, false);
        }
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        Initialize(vertices);

        while (true)
        {
            KeyValuePair<int, bool>[] left = _verticesBuffer.Where(buf => !buf.Value).ToArray();
            if (left.Length <= 3)
            {
                break;
            }
            DetecteTriangle();
        }

        int[] keys = _verticesBuffer.Keys.ToArray();
        foreach (int key in keys)
        {
            if (!_verticesBuffer[key])
            {
                _verticesBuffer[key] = true;
                _triangles.Add(key);
            }
        }

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _vertices.ToArray();

        mesh.triangles = _triangles.ToArray();
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle dicretion.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(_curIndex);
        _triangles.Add(_nextIndex);
        _triangles.Add(_prevIndex);

        bool isDtected = true;
        _verticesBuffer[_curIndex] = isDtected; 
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        foreach (var key in _verticesBuffer.Keys)
        {
            int index = key;

            if (_verticesBuffer[key])
            {
                continue;
            }

            // skip if index in detected three points.
            if (index == _curIndex || index == _nextIndex || index == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_vertices[index]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint).normalized;
        Vector3 edge2 = (PreviousPoiont - CurrentPoint).normalized;

        return Vector3.Cross(edge1, edge2);
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = FindNextIndex(_curIndex);
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 原点から最も遠い点を探す
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;

        foreach (var key in _verticesBuffer.Keys)
        {
            if (_verticesBuffer[key])
            {
                continue;
            }

            float dist = Vector3.Distance(Vector3.zero, _vertices[key]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = key;
            }
        }

        _curIndex = farIndex;
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 指定インデックスから調べて次の有効頂点インデックスを探す
    /// </summary>
    private int FindNextIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i + 1) % _vertices.Count;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }

    /// <summary>
    /// 指定インデックスから調べて前の有効頂点インデックスを探す
    /// </summary>
    private int FindPrevIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i - 1) >= 0 ? i - 1 : _vertices.Count - 1;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }
}

クリック位置を点群として採用する

さて、任意の多角形(点群)から三角形(ポリゴン)に分割する処理ができました。
あとはドラッグした位置の点を取り、それを点群としてリスト化することで目的のことが達成できます。

今回はシンプルに以下のように点群を得る処理を書きました。

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

public class PointDrawer : MonoBehaviour
{
    [SerializeField]
    private DrawMesh _drawMesh;

    [SerializeField]
    private Material _dotMat;

    [SerializeField]
    private float _dotSize = 0.05f;

    [SerializeField]
    private Material _material;

    [SerializeField]
    private float _threshold = 0.1f;

    private float _sqrThreshold = 0;

    private List<Vector3> _samplingVertices = new List<Vector3>();

    private List<GameObject> _dotList = new List<GameObject>();
    private List<Vector3> _vertices = new List<Vector3>();
    private List<GameObject> _meshList = new List<GameObject>();

    /// <summary>
    /// Get average point.
    /// </summary>
    private Vector3 AveragePoint
    {
        get
        {
            Vector3 avg = Vector3.zero;
            for (int i = 0; i < _samplingVertices.Count; i++)
            {
                avg += _samplingVertices[i];
            }
            avg /= _samplingVertices.Count;

            return avg;
        }
    }

    private void Awake()
    {
        _sqrThreshold = _threshold * _threshold;
    }

    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            TryRaycast();
        }

        if (Input.GetMouseButtonUp(0))
        {
            GameObject go = _drawMesh.CreateMesh(_vertices);
            go.GetComponent<MeshRenderer>().material = _material;
            go.transform.position += go.transform.forward * -0.001f;
            _meshList.Add(go);
        }

        if (Input.GetKeyDown(KeyCode.Q))
        {
            Clear();
        }
    }

    /// <summary>
    /// Try raycast to the plane.
    /// </summary>
    private void TryRaycast()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, float.MaxValue))
        {
            if (_vertices.Count == 0)
            {
                AddVertex(hit.point);
                return;
            }

            if (_samplingVertices.Count == 0)
            {
                _samplingVertices.Add(hit.point);
                return;
            }

            float dist = (AveragePoint - hit.point).sqrMagnitude;
            if (dist >= _sqrThreshold)
            {
                AddVertex(hit.point);
            }
            else
            {
                _samplingVertices.Add(hit.point);
            }
        }
    }

    private void AddVertex(Vector3 point)
    {
        CreateDot(point);
        _vertices.Add(point);
        _samplingVertices.Clear();
    }

    /// <summary>
    /// Create dot for clicked poisition.
    /// </summary>
    /// <returns>Dot GameObject.</returns>
    private GameObject CreateDot(Vector3 position)
    {
        Debug.Log("Create dot.");

        GameObject dot = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        dot.transform.localScale = Vector3.one * _dotSize;
        dot.transform.position = position;
        dot.GetComponent<MeshRenderer>().material = _dotMat;
        Destroy(dot.GetComponent<Collider>());

        _dotList.Add(dot);

        return dot;
    }

    public void Clear()
    {
        for (int i = 0; i < _dotList.Count; i++)
        {
            Destroy(_dotList[i]);
        }
        _dotList.Clear();

        for (int i = 0; i < _meshList.Count; i++)
        {
            Destroy(_meshList[i]);
        }
        _meshList.Clear();
    }
}

実装は大したことしてないですが、ドラッグ中(マウスダウン中)にRaycastを行って点の位置を特定、さらにそれを即座に採用せず、閾値以上動いたらそれを点として採用する、という感じです。

閾値以上移動したかどうかは、現在のマウス位置を毎フレーム取り、それの平均位置から現在のマウス位置との距離で判定しています。

このあたりは、要は点群が得られればいいだけなので如何用にでも実装できるかと思います。

まとめ

点群が用意できれば、それをポリゴンに分解できるので、あとはそれに対してやりたいことを実装すれば完了です。
今回はマスクとして使いたかったので、作ったメッシュにマスク用シェーダ(マテリアル)を割り当てて実際は利用しました。

ちなみに、(たまたまかもしれませんが)多少立体的になってもちゃんとポリゴンが形成されたのでもしかしたら3Dでも応用できるかもしれません。

AndroidのSpeechRecognizerをネイティブプラグイン化してUnityで使う

概要

UnityでAndroidのネイティブな音声認識機能を利用したかったのでプラグインから作成してみました。
今回は作成方法などのまとめです。

なお、ネイティブプラグイン自体の作成方法については以前書いたのでそちらを参照ください。

edom18.hateblo.jp

今回実装したものを実際に動かした動画です↓



音声認識する部分の処理を書く

さて、さっそくプラグイン部分のコードを。
プラグイン自体の作成方法は前回の記事を見ていただくとして、今回はプラグイン部分のみを抜き出しています。

プラグイン用のプロジェクトを作成したら、以下のように音声認識エンジンを起動するクラスを実装します。

package com.edo.speechplugin.recoginizer;

import android.content.Context;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.content.Intent;

import static com.unity3d.player.UnityPlayer.UnitySendMessage;

public class NativeSpeechRecognizer
{
    static public void StartRecognizer(Context context, final String callbackTarget, final String callbackMethod)
    {
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName());

        SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
        recognizer.setRecognitionListener(new RecognitionListener()
        {
            @Override
            public void onReadyForSpeech(Bundle params)
            {
                // On Ready for speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onReadyForSpeech");
            }

            @Override
            public void onBeginningOfSpeech()
            {
                // On begining of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "OnBeginningOfSpheech");
            }

            @Override
            public void onRmsChanged(float rmsdB)
            {
                // On Rms changed.
                UnitySendMessage(callbackTarget, callbackMethod, "onRmsChanged");
            }

            @Override
            public void onBufferReceived(byte[] buffer)
            {
                // On buffer received.
                UnitySendMessage(callbackTarget, callbackMethod, "onBufferReceived");
            }

            @Override
            public void onEndOfSpeech()
            {
                // On end of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onEndOfSpeech");
            }

            @Override
            public void onError(int error)
            {
                // On error.
                UnitySendMessage(callbackTarget, callbackMethod, "onError");
            }

            @Override
            public void onResults(Bundle results)
            {
                // On results.
                UnitySendMessage(callbackTarget, callbackMethod, "onResults");
            }

            @Override
            public void onPartialResults(Bundle partialResults)
            {
                // On partial results.
                UnitySendMessage(callbackTarget, callbackMethod, "onPartialResults");
            }

            @Override
            public void onEvent(int eventType, Bundle params)
            {
                // On event.
                UnitySendMessage(callbackTarget, callbackMethod, "onEvent");
            }
        });

        recognizer.startListening(intent);
    }
}

上記はリスナーの登録の雛形です。
Unityの機能であるUnitySendMessageを使っていますが詳細は後述します。

SpeechRecognizer#setRecognitionListenerでリスナを登録し、SpeechRecognizer#startListening音声認識エンジンを起動します。

認識した文字列を受け取る

認識した文字列を受け取る箇所については認識した際のコールバックで文字列を取り出します。

@Override
public void onResults(Bundle results)
{
    ArrayList<String> list = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
    String str = "";
    for (String s : list)
    {
        if (str.length() > 0)
        {
            str += "\n";
        }
        str += s;
    }

    UnitySendMessage(callbackTarget, callbackMethod, "onResults\n" + str);
}

基本的にプラグインとの値の受け渡しは文字列で行うのが通常のようです。
あとはUnity側で文字列を受け取り、適切に分解して利用することで無事、Unity上で音声認識を利用することができるようになります。

Unityの機能を利用できるようにclasses.jarをimportする

音声認識したあと、それをコールバックするためUnity側にメッセージを送信する必要があります。
その際、Unity側の実装を呼び出す必要があるため、それを利用するためにclasses.jarをimportしておく必要があります。

import先はモジュールのlibsフォルダ内です。

なお、該当のファイルは以下の場所にあります。(環境によってパスは読み替えてください)

D:\Program Files\Unity2017.3.1p4\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar

Gradle設定にclasses.jarを含めないよう追記する

classes.jarは、UnitySendMessageを使うのに必要ですが、aarに含まれてしまうとUnityでのビルド時にエラーが出てしまうため、aarに含めないよう設定ファイルに記述する必要があります。

以下を、モジュール用のbuild.gradleに追記します。

android.libraryVariants.all{ variant->
  variant.outputs.each{output->
    output.packageLibrary.exclude('libs/classes.jar')
  }
}

全体としては以下のようになります。

plugins {
    id 'com.android.library'
}

android {
    namespace 'com.edo.speechrecognizer'
    compileSdk 32

    defaultConfig {
        minSdk 23
        targetSdk 32

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

android.libraryVariants.all{ variant->
    variant.outputs.each{output->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.5.1'
    implementation 'com.google.android.material:material:1.7.0'
    implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

設定ファイルはこれです。

ちなみに設定しないと、

com.android.build.api.transform.TrasnformException: com.android.idle.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.dex.DexException: Multiple dex files define Lbitter/jnibridge/JNIBridge$a;

みたいなエラーが出ます。

[2023.03.04 追記]

どうやら上の記述だとビルド時に含まれてしまうようになったっぽいです。
ので、依存関係の解決を以下のように変更します。( compileOnly に変更する)

- implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
+ compileOnly fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])

AndroidManifestに録音権限を追記する

当然ですが、音声認識を利用するためにはマイクからの音を利用する必要があるため、AndroidManifestに下記の権限を追記する必要があります。

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Unity側から呼び出す処理を実装する

プラグインが作成できたらそれを利用するためのC#側の実装を行います。
以下のようにしてプラグイン側の処理を呼び出します。

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

public class AndroidSpeechRecognizer : MonoBehaviour
{
    [SerializeField]
    private Text _text;

    private void Update()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.touches[0];
            if (touch.phase == TouchPhase.Began)
            {
                StartRecognizer();
            }
        }
    }

    private void StartRecognizer()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeRecognizer = new AndroidJavaClass("com.edo.speechplugin.recoginizer.NativeSpeechRecognizer");
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeRecognizer.CallStatic(
                "StartRecognizer",
                context,
                gameObject.name,
                "CallbackMethod"
            );
        }));
#endif
    }

    private void CallbackMethod(string message)
    {
        string[] messages = message.Split('\n');
        if (messages[0] == "onResults")
        {
            string msg = "";
            for (int i = 1; i < messages.Length; i++)
            {
                msg += messages[i] + "\n";
            }

            _text.text = msg;
            Debug.Log(msg);
        }
        else
        {
            Debug.Log(message);
        }
    }
}

ネイティブプラグイン側で音声認識の結果をUnitySendMessageによってコールバックするようになっているので、それを受け取るコールバック用メソッドを実装しています。

プラグイン側では文字列でメソッド名を指定してコールバックを実行するので、プラグイン側にメソッド名を適切に渡す必要があります。

あとは認識した文字列を元に、行いたい処理を実装すれば音声認識を利用したアプリを制作することができます。

トラブルシューティング

もし、プラグインは実行できるのにすぐにエラーで停止してしまう場合は、マイクの権限が付与されていない可能性があるのでアプリの設定を見直してみてください。

参考記事

今回のプラグイン作成には以下の記事を参考にさせていただきました。

indie-du.com

fantom1x.blog130.fc2.com

fantom1x.blog130.fc2.com

Unity向けにAndroidのネイティブプラグインを作成する

概要

Unity向けに、Androidのネイティブ機能を呼び出す部分が作りたくて色々調べたのでまとめておきます。

今回は特にこちらの記事を参考にさせていただきました。

indie-du.com

Android Studioで新規プロジェクトを作成する

今回作成するのはaar(Android Archive)なので、新規で作成するプロジェクトは空の、ごくシンプルな状態で作成して問題ありません。(プロジェクト作成後に、モジュール追加してそっちでコードを書くので、通常のapk作成フローとは異なります)

f:id:edo_m18:20180320163214p:plain

次に、Phone and Tabletを選択し、Minimum SDKのバージョンを設定します。
ここは、後々、Unityの設定でも最低バージョンを指定する際に同じバージョンにしないとエラーが出るので、それなりに小さいのを選んでおいたほうがいいでしょう。

f:id:edo_m18:20180320163359p:plain

そしてAdd No Activityを選択してプロジェクトを作成します。

f:id:edo_m18:20180320163520p:plain

ライブラリ用モジュールを作成する

プロジェクトが作成できたら、ライブラリ用のモジュールを新規追加します。

f:id:edo_m18:20180320163800p:plain

Android Libraryを選択して追加します。

f:id:edo_m18:20180320163843p:plain

プラグイン名となるモジュール名を入力します。

f:id:edo_m18:20180320164033p:plain

すると、プロジェクトビューに、先ほど入力した名前でモジュールが追加されます。

f:id:edo_m18:20180320164210p:plain

Unityからアクセスするクラスを実装する

今回はネイティブの機能(ダイアログ)を簡単に呼び出すだけなので、なにも継承しないシンプルなJavaクラスをモジュールに追加します。
追加するには、Androidビュー内のモジュール名上で右クリックしてNew > Java Classを選択します。

f:id:edo_m18:20180320164823p:plain

そして、NativeDialogというクラス名でファイルを作成し、参考にさせていただいた記事同様の記述を行います。

引用させてもらうと、以下のコードになります。ネイティブのダイアログを表示する、というものです。
パッケージ名は自身の環境に合わせてください

package jp.co.test.dialog_plugin;

import android.app.AlertDialog;
import android.content.Context;

public class NativeDialog {

    static public void showMessage(Context context, String title, String message) {

        new AlertDialog.Builder(context)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("YES, YES, YES!", null)
                .setNegativeButton("...No?", null)
                .show();
    }
}

Gradleを使ってビルドする

ファイルが追加されたら、Gradleを使ってビルドします。

Android Studioの右側にある「Gradle」タブを開き、:plugin-name(名称は自身で設定したもの)の下にある「Tasks > build > assemble」をダブルクリックして実行します。

f:id:edo_m18:20180320170330p:plain

基本はこれでビルドされるはずですが、自分の環境ではちょっとエラーで躓いてしまったので、同様のエラーが出た場合は、後述の対策を参照してみてください。

エラー内容

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

ビルドが成功すると、プロジェクトフォルダ内(※)の「myplugin/build/outputs/aar」に、ビルドされたaarファイルが作成されています。

Android Studioのプロジェクトビューはいくつかのモードがあり、デフォルトはAndroidになっているので、これをProjectに変更するとフォルダ構造が見れるようになるので、そちらのモードにすると表示されるようになります。

f:id:edo_m18:20180320185855p:plain

上記dialog-pluginというモジュール名で作成した時のキャプチャです。
****-debug.aar****-release.aarが作成されます。今回はdebugのほうをimportしました。

これを、Unityのプラグインフォルダにコピーして利用します。

f:id:edo_m18:20180320170912p:plain

Unityから、ネイティブプラグインの機能を呼び出す

プラグイン部分が作成できたので、あとはこれをC#から読み込み、利用するコードを書きます。
以下のように、AndroidJavaClassAndroidJavaObjectを利用して構築します。

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

public class AndroidDialogTest : MonoBehaviour
{
    private void Update()
    {
        if (Input.touchCount > 0)
        {
            ShowDialog();
        }
    }

    private void ShowDialog()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeDialog = new AndroidJavaClass("plugintest.edo.com.dialog_plugin.NativeDialog");

        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeDialog.CallStatic(
                "showMessage",
                context,
                "テスト",
                "ほげ"
            );
        }));
#endif
    }
}

Android Studioのビルドでハマった

Android開発をしている人にとっては多分なんてことはないことなんだと思いますが、Android Studioを使ったことがほぼないので、細かなエラーに悩まされました。

こちらの記事を参考に、ごく簡単なプラグイン作成を試してみたところ、

indie-du.com

以下のようなエラーが。

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

どうも調べていくと、Android Support Libraryを最新にしろ、ということのよう。
ただ、調べてもSDK Managerでチェック入れてインストール、みたいなのしか出てこないのに、利用しているAndroid Studioだとそもそもそのチェック項目がない。なんでやねん。

と思っていたら、こちらにやり方が書いてありました。

qiita.com

要は、build.gradleに依存関係を書け、ということのよう。

以下のように追記しました。

allprojects {
  repositories {
    jcenter()
    maven { url 'https://maven.google.com' } // 変更点
  }
}

こちらも参考にさせていただきました。

animane.hatenablog.com

gradle

ちなみに余談ですが、最初、gradle自体が分かっていなかったんですが、ビルド自動化ツールでした。

GradleはApache AntやApache Mavenのコンセプトに基づくオープンソースビルド自動化システムであり、プロジェクト設定の宣言にはApache Mavenが利用するXML形式ではなくGroovyベースのドメイン固有言語 (DSL) を採用している[2]。Gradleはタスクの起動順序の決定に有向非巡回グラフ(英: Directed Acyclic Graph、DAG)を利用する。

[出典: wikipedia]

Android Studioではこれを取り入れていて、専用のタブやビューが存在します。
build.gradleはテキストファイルで、自動ビルドツール用の設定ファイルです。
(なので、プロジェクト内のどこかに存在しているので、それを編集します)

まとめ

以上で、ネイティブのダイアログを表示するだけのプラグインが作成できました。
最後に、全体の流れをまとめとして書いておきます。

  1. Android Studioで空プロジェクトを作る
  2. 作成したプロジェクトにモジュールを新規追加する
  3. モジュールに、ネイティブ機能を実行する処理を追加・実装する
  4. Gradleを使ってモジュールをビルドする
  5. 生成されたaarファイルをUnityにimportする
  6. Unityから、aar内の実装を呼び出す処理を書く

大まかにはこんな流れで実装していきます。
ビルドエラー周りが少々厄介ですが、それをクリアしてしまえば、比較的ネイティブの機能を実行するのは容易になるかなと思います。

Unreal Engine C++ 逆引きメモ

目次

概要

まだまだUEに慣れていないので、色々なことを忘れる前に逆引きでメモしておきます。
(なので、随時更新予定)

また、UEでC++を書くにあたって理解しておかなければならない点として、標準のC++とは「異なる」という点です。
どういうことかと言うと、UEではガベージコレクション、通称GCと呼ばれる仕組みを導入しています。

しかし、当然ながらC++にはガベージコレクションはありません。
そこでUEでは、独自のインスンタンス生成の仕組みや、マクロを用いたプリプロセッサ経由で様々な、GCのための準備をしてくれます。

そのため、標準のC++とUEで使うC++、言ってみればUE C++とで作法が違う、という点を覚えておく必要があります。

生成・取得・削除

C++クラスの生成

通常のC++では、newを用いてインスタンスを生成します。
しかし、UE C++ではインスタンスの生成方法に違いがあります。理由は前述のように、GC対象として管理するためです。
最近ではスマートポインタを使ったりしますが、それと似た感じですね。

UMyClass MyClass = NewObject<UMyClass>();

// オーナーを指定する場合は引数に入れる
// UMyClass MyClass = NewObject<UMyClass>(Owner);

当然のことながら、UE管理下に置かないような純粋なC++で書く処理についてはこの作法は適用されません。

コンストラクタ内でNewObjectは使えない

どうやら、コンストラクタ内では上記のNewObjectは使えないようです。(使うとクラッシュする)

ではどうするかというと、FObjectInitializer::CreateDefaultSubobjectを利用します。
FObjectInitializerは、コンストラクタ引数に指定しておくと、UEシステムが適切に渡してくれるようになっています。(詳細は後述)

以下のようにコンストラクタを定義することで利用できるようになります。

UAnyClass::UAnyClass(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // オブジェクト生成
    UAthorClass* instance = ObjectInitializer.CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

しかし実はこのCreateDefaultSubobjectは、UObject基底クラスでラッパーが実装されているようで、オブジェクトを生成する目的だけであれば、CreateDefaultSubobjectを直に呼ぶことで同様のことを達成することができます。

UAnyClass::UAnyClass()
{
    // オブジェクト生成
    UAthorClass* instance = CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

なおこのコンストラクタに引数を指定した場合としない場合の挙動の差ですが、UEシステムが自動的に生成する****.generated.h内にてマクロが生成され、コンストラクタの定義に応じて書き換わるよういなっているようです。

詳細はこちら([UE4] ObjectInitializerでコンポーネント生成を制御する | 株式会社ヒストリア)の記事をご覧ください。

PlayerController / PlayerPawnを取得する

ゲーム開始時に生成されたプレイヤーコントローラ / プレイヤーポーンを取得するには、Kismet/GameplayStatics.hを読み込む必要があります。

PlayerController

#include "Kismet/GameplayStatics.h"

APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0);
if (PlayerController)
{
    // do anything.
}

PlayerPawn

#include "Kismet/GameplayStatics.h"

// UGameplayStatics::GetPlayerPawnを介して取得し、適切にキャストする
AAnyCharacter* MyCharacter = Cast<AAnyCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));
if (MyCharacter)
{
    // do anything.
}

GameModeとの関連

なお、このPlayerControllerPlayerPawnの関係は、PawnをコントロールするのがPlayerControllerの役割です。
これらの設定はGameModeに設定するようになっており、またGameModeはプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。

詳細はこちら↓

msyasuda.hatenablog.com

ワールドに存在するアクターをすべて取得する

#include "Kismet/GameplayStatics.h"

// find all AnyActors
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AAnyActor::StaticClass(), FoundActors);

for (auto Actor : FoundActors)
{
    AAnyActor* AnyActor = Cast<AAnyActor>(Actor);
    if (AnyActor )
    {
        // do anything.
    }
}

アクターオブジェクトを生成する

オブジェクトの生成には、UWorldクラスのメソッドを利用します。

// FActorSpawnParametersを使うのに必要
#include "Runtime/Engine/Classes/Engine/World.h"

// AActor::GetWorldから、UWorldを得る
UWorld* const World = GetWorld();

// Nullチェック
if (!World)
{
    return;
}

FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotator(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;

AAnyActor* AnyActor = World->SpawnActor<AAnyActor>(ActorBP, Location, Rotation, SpawnParams);

削除中かを知る

まだ理解が浅いですが、Unityと同様、Destroyを実行しても即座にメモリから消えるわけではなく、ゲームとして破綻しないよう様々な終了処理が存在するはずです。
そのため、削除中、という状態が存在します。
それをチェックするには以下のようにします。

AActor* anyActor = ...;
anyActor->IsPendingKill();

移動

アクターを移動させる

AActorクラスにはGetActorLocationSetActorLocationがあるのでこれを利用する。

FVector location = actor->GetActorLocation();
location.X += 10.0f;
actor->SetActorLocation(location);

ちなみに、SetActorLocationの定義を見ると以下のようになっています。

/** 
 * Move the actor instantly to the specified location. 
 * 
 * @param NewLocation  The new location to teleport the Actor to.
 * @param bSweep       Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something.
 *                     Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.
 * @param Teleport     How we teleport the physics state (if physics collision is enabled for this object).
 *                     If equal to ETeleportType::TeleportPhysics, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
 *                     If equal to ETeleportType::None, physics velocity is updated based on the change in position (affecting ragdoll parts).
 *                     If CCD is on and not teleporting, this will affect objects along the entire swept volume.
 * @param OutSweepHitResult The hit result from the move if swept.
 * @return Whether the location was successfully set if not swept, or whether movement occurred if swept.
 */
bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);

引数を色々変更することによって、移動後の物理干渉などに対する設定や演算結果を受け取ることができるようです。

カメラの向いている方向に移動させる

まず、Worldからカメラマネージャを取得し、そこからカメラの前方を取得、それを元にアクターを移動させる、という手順で行います。

#include "Engine.h" // GEngineを使うのでインクルードしておく

void AMyActor::BeginPlay()
{
    if (GEngine != nullptr)
    {
        // CameraManagerをワールドから取得する
        CameraManager = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager;
    }
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FRotator cameraRot = CameraManager->GetCameraRotation();
    FVector dir = cameraRot.Vector();
    dir.Normalize();

    FVector location = GetActorLocation();
    location += dir * 10.0f * DeltaTime;
    
    SetActorLocation(location);
}

コンポーネント

Blueprintに、カスタムコンポーネントを表示させる

UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class PROJECTNAME_API UHogeMovementComponent : public UPawnMovementComponent
{
    // ... 略 ...
}

こんな感じで、UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))を指定すると、BPのAddComponentのリストに表示されるようになります。

コンポーネントのセットアップ

コンポーネントを適切にセットアップしないと、TickComponentが呼ばれないなど問題が出るため、適切にセットアップする必要があります。

void USampleComponent::AttachSampleComponent(AActor* Actor)
{
    if (!Actor)
    {
        return;
    }

    // Componentを新規生成
    UOtherComponent* Comp = NewObject<UOtherComponent>(Actor);

    // アクタにアタッチする
    Actor->AddInstanceComponent(Comp);

    // UActorComponent::RegisterComponentで、イベントループに登録する
    Comp->RegisterComponent();
}

追加されているコンポーネントを取得する

TArray<AnyComponent*> components;
GetComponents<AnyComponent>(components);

// コンポーネントの数を確認
UE_LOG(LogTemp, Log, TEXT("Count: %d"), components.Num());

Ownerを取得する

ComponentはActorのコンポーネントとして振る舞うため、オーナーを取得して操作することが増えるかと思います。
オーナーの取得は以下のようにします。

AActor *owner = GetOwner();

ComponentからInputを使う

InputまわりはUInputComponentが司ります。
UActorComponentではUInputComponentを保持していないので、オーナーなどから取得して適切にセットアップする必要があります。

PawnクラスのサブクラスなどではSetupPlayerInputComponentのタイミングでUInputComponentが渡ってくるので、そこでセットアップの機会があるようです)

https://docs.unrealengine.com/latest/INT/Programming/Tutorials/PlayerCamera/3/docs.unrealengine.com

AActor *actor = GetOwner();
actor->InputComponent->BindAction("Fire", IE_Pressed, this, &UAnyComponent::FireHandler);

void UAnyComponent::FireHandler()
{
    UE_LOG(LogTemp, Log, TEXT("Fire!!!");
}

※ Input系については、UE4のお作法に則って適切にセットアップする必要があります。

セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。

docs.unrealengine.com

Static Meshを生成する

USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
UStaticMeshComponent *mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("mesh"));
mesh->SetupAttachment(root);

static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("[reference]"));
UStaticMesh *asset = MeshAsset.Object;

mesh->SetStaticMesh(asset);

ちなみに、[reference]の部分は、UEエディタのアセットのコンテキストメニュー内のCopy Referenceから得ることができます。

f:id:edo_m18:20180208102926p:plain

参考:
forums.unrealengine.com

Colliderを設定する

コライダの設定にはUSphereComponentなどを利用します。
詳細パネルではSphere Collisionとか表示されるやつです。

AMyPawn::AMyPawn()
{
    // ... 中略 ...

    // Sphereコライダを生成
    USphereComponent *sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
    sphere->InitSphereRadius(100.0f);
    sphere->SetSimulatePhysics(false);
    sphere->SetCollisionProfileName(TEXT("BlockAll"));

    // Sphereの見た目を生成
    UStaticMeshComponent *sphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereVisual"));
    sphereVisual->AttachTo(sphere);

    static ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (meshAsset.Succeeded())
    {
        sphereVisual->SetStaticMesh(meshAsset.Object);
        sphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
        sphereVisual->SetWorldScale3D(FVector(0.8f));
    }
}

セットアップ時に行っているのは3点。

  • sphere->InitSphereRadius(100.0f);
  • sphere->SetSimulatePhysics(false);
  • sphere->SetCollisionProfileName(TEXT("BlockAll"));

です。それぞれ上から、

  • 球体の半径の設定
  • 物理挙動させるか(falseの場合は、Unityで言うIsKinematic = trueの状態)
  • コリジョンの仕方

となります。
特に最後の「コリジョンの仕方」は、衝突するのか、イベントのみ(overlap)なのか、というところ設定するので、意図した通りに設定しておかないと「衝突しないじゃん」となるので注意です。(というか、ここを勘違いしていてずっと衝突しなくてちょっとハマった)

コリジョンを処理する

コリジョンを設定したあとは、コリジョン発生時になにかしらの処理をしたい場合があります。
その場合に使えるのが、ColliderのもつGetOverlappingActorsメソッドです。

コライダの範囲にあるAActorオブジェクトを取得してくれます。

※ 以下のコードは、公式のチュートリアルの中で使われているコードの抜粋です。

void ABatteryCollectorCharacter::CollectPickups()
{
    // Get all overlapping Actors and store them in an array
    TArray<AActor*> CollectedActors;
    CollectionSphere->GetOverlappingActors(CollectedActors);

    // For each Actor we collected
    for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
    {
        // Cast the actor to APickup
        APickup* const TestPickup = Cast<APickup>(CollectedActors[iCollected]);

        // If the cast is successful and the pickup is valid and active
        if (TestPickup && !TestPickup->IsPendingKill() && TestPickup->IsActive())
        {
            // call the pickup's WasCollected function
            TestPickup->WasCollected();

            // Deactivate the pickup 
            TestPickup->SetActive(false);
        }
    }
}

Blueprintで利用する

C++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTYUFUNCTIONです。
これらを適切に設定することで、ブループリントから設定できたり、あるいはGetter / Setterとして機能したり、あるいはブループリントで実装を促す、なんてこともできるようになります。

以下に、キーワードと意味を、よく見るものを抜粋して記載しておきます。
※ 英語ドキュメントの翻訳(意訳)なので、詳細についてはドキュメントをご覧ください。

Keyword 意味
BlueprintImplementableEvent ブループリント(ノード)でオーバーライドするように促す。そのため、Body(実装)部分は書いてはならない。UEにより、(ブループリントで)オーバーライドされた本体を実行するProcessEventを呼び出すためのコードが自動生成される
BlueprintNativeEvent BlueprintImplementableEvent同様、ブループリントでオーバーライドするようデザインされたものであるが、違いとしてはC++による実装を行う点。実装本体は[FunctionName]_Implementationを実装する必要がある。元の[FunctionName]内には、自動生成コードとして、[FunctionName]_Implementationを呼び出すコードが追加される(※1)
BlueprintPure このキーワードをつけられた関数は、副作用を起こさないものとしてマーク付けられ、さらにBlueprintCallableを意味します。Getメソッドの実装に適したものです。さらに、non-pureになるようにconst functionでfalseとマークすることもできる(※2)
BlueprintCallable Blueprintから呼び出しできるようにマークする
Category カテゴリを定義する。設定すると、Blueprint上でカテゴライズされて表示される

※1 [FunctionName]\_Implementationを実装しないとコンパイルエラーになる。定義すると、BPのEvent Graphで配置して利用できるようになる。以下、サンプル↓

UFUNCTION(BlueprintNativeEvent)
void HogeHoge();

virtual void HogeHoge_Implementation();

f:id:edo_m18:20180225214321p:plain

※2 PureとNon-Pureについて書かれている記事があったので、詳細はこちらをご覧ください→ [UE4] Pure関数とNonPure関数|株式会社ヒストリア

ドキュメント:UFUNCTION - Unreal Engine Wiki

Blueprintからプロパティに値を設定できるようにする

コンポーネントを作成し配置しても、適切にマクロを設定しておかないとBPからプロパティに対して値を設定することができません。

以下のように設定することでそれが可能となります。

UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn)

ドキュメント: UPROPERTY - Epic Wiki


その他

enumを定義する

l参考にしていたVideo Tutorialは若干古いバージョンだったため、その中で説明されていたenumだとエラーが出てコンパイルができませんでした。
ということで、いちおうメモ。

UENUM(BlueprintType)
enum class EHogeEnum : uint8
{
    EFuga,
    EFoo,
    EBar,
};

と、uint8を継承した形で宣言しないとなりません。

タイマーを利用する

タイマーの利用には、FTimerManagerクラスを利用します。
以下の例では、自身を再帰的にタイマーで呼び出します。

// header
FTimeHandle Timer;
void AAnyActor::TimerHandler();

// implementation
void AAnyActor::TimerHandler()
{
    // &AActor::GetWorldTimerManager
    float Delay = FMath::FRandRange(1.0f, 2.0f);
    GetWorldTimerManager().SetTimer(Timer, this, &AAnyActor::TimerHandler, Delay, false);
}

ドキュメントはこちら

docs.unrealengine.com


トラブルシューティング

TickComponentが呼ばれなくなった!

C++を書いていて、突然、少し前まで正常に動いていたTickComponentが動かなくなるケースがありました。

色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。

最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓

community.gamedev.tv

まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。

なので、追加し直しで直ったのではないかなと。

ちなみに、TickComponentを呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。

usagi.hatenablog.jp

case内の初期化

これはUEというよりC++の問題ですが、switch文内で初期化を伴う処理を書いている場合、case文を{}で囲まないとコンパイルエラーとなるようです。


その他Tips

色々な値をログ出力

Log Fomatting

  • LogMessage
UE_LOG(LogTemp, Log, TEXT("Hoge"));
  • Log an FString
FString anyString = ...;
UE_LOG(LogTemp, Log, TEXT("Log: %s"), *anyString);
  • Log an Bool
bool anyBool = ...;
UE_LOG(LogTemp, Log, TEXT(Bool value: %s"), anyBool ? TEXT("True") : TEXT("False")); 
  • Log an Int
int anyInt = ...;
UE_LOG(LogTemp, Log, TEXT("Int value: %d"), anyInt);
  • Log a Float
float anyFloat = ...;
UE_LOG(LogTemp, Log, TEXT("Float value: %f"), anyFloat);
  • Log an FVector
FVector anyVector = ...;
UE_LOG(LogTemp, Log, TEXT("FVector value: %s"), *anyVector.ToString());
  • Log an FName
FName anyName = ...;
UE_LOG(LogTemp, Log, TEXT("FName value: %s"), *anyName.ToString());

ドキュメントはこちら → Logs, Printing Messages To Yourself During Runtime

Tickメソッド外で時間を扱う

Tickメソッド内では、引数にDeltaTimeが渡ってくるのでそれを利用すればいいですが、それ以外の場合はワールドから取得する必要があります。

void AnyMethod()
{
    float time = GetWorld()->GetTimeSeconds();
    float deltaTime = GetWorld()->GetDeltaSeconds();

    UE_LOG(LogTemp, Log, TEXT("Time: %f, DeltaTime: %f"), time, deltaTime);
}

番外編

ちょっと昔に書いた記事ですが、Cocos2D-xでアプリを作る際にまとめた、C++関連の記事です。
UEとは関係ない部分もありますが、C++的なところは同じなので紹介。

qiita.com

UnityのネイティブプラグインをC/C++で作成する準備

概要

ネイティブで書かれたプラグインを、使うことはあっても自分で書いたことがなかったのでHello Worldしてみたメモです。

ちなみに、できるだけ動作を把握する意味も込めてコマンドラインで作ることを前提としています。

開発環境を整える

最初、Git Bashを使っていたのでそこにgccコマンドが入っていてそれを利用していたのですが、ごく簡単な関数を書いてインポートしたところエラーが。
(と思ったら、会社のPCでの環境だった。いつgcc入れたっけな・・。デフォルトでは入ってませんでした)

$ Failed to load 'Assets/Plugins/************.dll', expected 64 bit architecture 

64bitを期待してるけど、32bit向けに作られたものですよ、ということ。
ならばと、64bit向けにコンパイルすればいいのね、はいはい、と思いつつ、-m64オプションを付けて実行するも・・・

$ sorry, unimplemented: 64-bit mode not compiled in

MINGW64って書いてあるのに、なんでさ。

その後、色々調べてみたら、MINGW64自体を使うこと自体は間違っていない模様。
64bit版で書き出せるコマンドがあるらしく探してみるものの、どうもGit Bashはビルドが違うのか、該当のコマンドが見当たりませんでした。

なので、別途新しくMINGW64をインストール。
このへん(Windowsの無料で使える 64bit/32bit C/C++コンパイラ)を参考にしました。

x86_64向けにコンパイルするには、以下の位置にあるコマンドを利用します。

path/to/location/mingw-w64/x86_64-7.2.0-win32-seh-rt_v5-rev1/mingw64/bin/x86_64-w64-mingw32-gcc

コンパイルする

これに、-m64オプションを付けてコンパイルしたところ、無事に64bit向けにコンパイルすることができました。

$ x86_64-w64-mingw32-gcc -m64 -c anyplugin.c

そして、生成されたオブジェクトファイル(.o)を、DLLに変換します。

$  x86_64-w64-mingw32-gcc -shared -o anyplugin.dll anyplugin.o

これで無事にDLLファイルが作成されます。

※ Pathを通して↑のコマンドを使ってます。

コンパイルされたファイルのアーキテクチャを確認する

ちなみに、すでにコンパイルされているdllやオブジェクトファイルがどのアーキテクチャ向けにビルドされているかを確認するには、Visual Studioに同梱されているdumpbin.exeを利用すると分かるようです。

該当のexeは以下の場所らへんにあります。(インストール環境による)

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin

このあたりの詳細は以下を参照ください。

rms-099.hatenablog.jp

d.hatena.ne.jp

そして、チェックしたいファイルを引数に以下のように実行します。

$ dumpbin /headers anyplugin.o

すると、ダーっと色々な情報が表示されますが、先頭のほうにある、

// ... 略 ...

FILE HEADER VALUES
            8664 machine (x64)

// ... 略 ...

を見てみると、x64と書かれているので、64bit向けにコンパイルできていることが確認できます。

Cで書いたDLLを読み込んでUnityで実行する

さて、これでDLLを作成する準備が整いました。
最後に、ごく簡単なサンプルを書いて終わりにしたいと思います。

Cで処理を書く

今回はサンプルなので、Cで簡単な処理を書いてみます。

// plugin.c
int Add(int a, int b)
{
    return a + b;
}

さて、これをUnityのC#側で利用できるようにします。
前述のようにコンパイルを行い、DLLファイルを作成します。

そしてそれを、Assets/Pluginsフォルダにコピーし、以下のようにC#側で呼び出します。

// ... 略 ...

using System.Runtime.InteropServices; // <- DllImportを使うために追加

public class AnyClass : MonoBehaviour
{
    private void Start()
    {
        int test = Add(1, 2);
        Debug.Log(test); // -> 3
    }


    // プラグインのファイル名を指定する
    [DllImport("plugin")
    private static extern int Add(int a, int b);
}

橋渡しができたら、あとは実際の処理を書いていくことでネイティブ側のコードを実行することができるようになります。

参考

なお、共有ライブラリのコンパイル周りについては、過去にQiita記事に書いているので、よかったら見てみてください。

qiita.com

UnrealEngineでVRことはじめ(C++編)

概要

Unreal Engineで、VR向けコンテンツをC++を使って作るための「ことはじめ」を書いていきたいと思います。
大体の書籍を見ていると、ブループリントの説明しかなくてあまりC++に対して言及しているものが少なく感じます。

ただ、やはり自分としてはできるだけコードを書いて作っていきたいので、UE4のC++入門などを通して得た内容をメモとして書いていこうと思います。

新規プロジェクト作成

まずは、なにはなくともプロジェクトを作らないと始まりません。
ということで、プロジェクトを作成します。
今回はイチからVR関連の操作を行うまでをやるので、空のプロジェクトから開始してみます。

f:id:edo_m18:20180207111037p:plain

C++ファイルを追加する

さて、プロジェクトが出来たのでまずはC++のファイルを作成してみます。
「Contents Browser」の「Add New」から、「New C++ Class...」を選択します。

※ ちなみに、エディタの言語を英語にしています。なぜなら、コンポーネントの検索などに日本語でしか反応しないコンポーネントとかもあって作業しづらいからです。

f:id:edo_m18:20180207110057p:plain

続くウィンドウで、Pawnをベースクラスとして作成します。

f:id:edo_m18:20180207110431p:plain

雛形を見てみる

さて、生成されたcppファイルを見てみると以下のように、すでにある程度雛形になった状態になっています。

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "VRPawn.generated.h"

UCLASS()
class MYPROJECT_API AVRPawn : public APawn
{
    GENERATED_BODY()

public:
    // Sets default values for this pawn's properties
    AVRPawn();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};

Unityの新規作成したC#コードと見比べても、あまり大きくは違いませんね。
Unityのものと同様、スタート時点で呼ばれるメソッド、アップデートごとに呼ばれるメソッドが最初から定義されています。

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:   
    // Called every frame
    virtual void Tick(float DeltaTime) override;

この部分ですね。
使い方もUnityのものとほぼ同様です。

ただ、少し異なる点としては、毎フレームのメソッド呼び出し(Tick)が必要かどうかをboolで指定する点でしょうか。

// Sets default values
AVRPawn::AVRPawn()
{
    // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
}

Updateごとにメソッドを呼ばれないようにするためには、このフラグをfalseにする必要があります。

マクロなどを見てみる

少しだけ脱線して、雛形に最初から挿入されているマクロなどをちょっとだけ覗いてみました。

// GENERATED_BODY
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D

// UCLASS
#if UE_BUILD_DOCS || defined(__INTELLISENSE__ )
#define UCLASS(...)
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

ふむ。よく分からんw
ただ、様々な機能をブループリントで利用するためだったりで、いたるところでマクロが使われているので、細かいところは後々追っていきたい。
ひとまずは以下の記事が参考になりそうです。

qiita.com

エディタでプロパティを表示させる

さて、話を戻して。

Unityのインスペクタに表示するのと似た機能がUEにもあります。
それがUPROPERTYマクロです。以下のように指定します。

public:
    UPROPERTY(EditAnywhere) int32 Damage;

UPROPERTY(EditAnywhere)を、エディタに公開したいメンバに追加するだけですね。

コンパイルすると以下のように、エディタ上にパラメータが表示されるようになります。

f:id:edo_m18:20180207104916p:plain

参考:

docs.unrealengine.com

docs.unrealengine.com

さて、以上でC++を書き始める準備が整いました。

ここからは実際の開発で使いそうな部分を含めつつ、VRのモーションコントローラを使うまでを書いていきたいと思います。

C++コードを書き始める

さぁ、ここから実際にC++コードを書いて、VRモーションコントローラを操作できるまでを書いていきたいと思います。

ログを出力する

まず始めに、現状を知る上で必要なログ出力から。
UEでログを出力するには以下のようにしります。

UE_LOG(LogTemp, Log, TEXT("hoge"));

また、オブジェクト名などを出力するには以下のようにします。

AActor *actor = GetOwner();
if (actor != nullptr)
{
    FString name = actor->GetName();
    UE_LOG(LogTemp, Log, TEXT("Name: %s"), *name);
}

TEXTに、引数を指定できるようにしつつ、*nameとして変数を指定します。

TEXTには、FStringではなくTCHAR*を指定しないとならないようです。
そのため、*FStringとして変換する必要があります。

変換関連については以下のTipsが詳しいです。

qiita.com

ログは、「アウトプットログ」ウィンドウに表示されます。

画面にログを表示する

コンソールだけでなく、ゲーム画面にもログを出力することができるようになっています。
画面へのログ出力にはGEngineを使うため、Engine.hをincludeする必要があります。

#include "Engine.h"

ログを出力するには以下のようにGEngine->AddOnScreenDebugMessageを利用します。

if (GEngine)
{
    GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("hoge")));
}

引数などの細かな項目はこちらの記事が詳しいです。

increment.hatenablog.com

MotionControllerComponentを使う

Oculusなどのモーションコントローラ(Oculus Touchなどのコントローラ)をC++から利用するには、適切なヘッダファイルなどのセットアップが必要となります。

ヘッダをinicludeする

まず、UMotionControllerComponentを利用するためにはMotionControllerComponent.hをincludeする必要があります。

ビルド設定にモジュールを設定する

UEに不慣れなので、地味にハマったところがここ。
どうやらUEでは、特定のC#スクリプトでビルド周りのセットアップなどが実行されているようで、それに対して適切に、利用するモジュールを伝えてやる必要があるようです。

具体的には、PROJECT_NAME.Build.csに適切に依存関係を設定してやる必要があります。
該当のファイルは、C++ファイルが生成されたフォルダに自動的に作られています。

e.g.) MyProjectというプロジェクトを作成した場合はMyProject/Source以下にあります。

該当のCSファイルに、以下のようにモジュール名を指定します。

// Fill out your copyright notice in the Description page of Project Settings.

using UnrealBuildTool;

public class MyProject : ModuleRules
{
    public MyProject(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });

        PrivateDependencyModuleNames.AddRange(new string[] { "HeadMountedDisplay" , "SteamVR" });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}

デフォルトで生成されていた部分からの差分は以下の部分です。

PrivateDependencyModuleNames.AddRange(new string[] { "HeadMountedDisplay" , "SteamVR" });

です。
プロジェクト生成直後はここが空の配列になっているので、ここに依存関係を追加してやる必要があります。(HeadMountedDisplaySteamVRのふたつ)

これで無事、エラーが出ずにコンパイルすることができるようになります。

逆に、この設定をしていないと以下のようなエラーが出力されてコンパイルに失敗します。

error LNK2019: unresolved external symbol "private: static class UClass * __cdecl UMotionControllerComponent::GetPrivateStaticClass(void)" (?GetPrivateStaticClass@UMotionControllerComponent@@CAPEAVUClass@@XZ) referenced in function "public: __cdecl AVRPawn::AVRPawn(void)" (??0AVRPawn@@QEAA@XZ)

以上を踏まえた上で、最後、MotionControllerComponentをセットアップしていきます。

ということで、UMotionControllerComponentC++から設定するサンプルコード。

まずはヘッダ。

// VRPawn.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "MotionControllerComponent.h"
#include "VRPawn.generated.h"

UCLASS()
class MYPROJECT_API AVRPawn : public APawn
{
    GENERATED_BODY()

private:
    UMotionControllerComponent *_leftMotionController;
    UMotionControllerComponent *_rightMotionController;

public:
    // Sets default values for this pawn's properties
    AVRPawn();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};

実装は以下になります。

// VRPawn.cpp

#include "VRPawn.h"
#include "Engine.h"

AVRPawn::AVRPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = root;

    _leftMotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionControllerLeft"));
    _rightMotionController = CreateDefaultSubobject<UMotionControllerComponent>(TEXT("MotionControllerRight"));
}

void AVRPawn::BeginPlay()
{
    Super::BeginPlay();
}

void AVRPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

// Called to bind functionality to input
void AVRPawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);
}

簡単なセットアップコードは以上です。

この状態でコンパイルし、シーンに配置すると以下のような状態になっていれば成功です。

f:id:edo_m18:20180207171131p:plain

これで、あとはOculusなどを起動するとOculus Touchなどの動きを反映してくれるようになります。

ただ、現状の状態はあくまでプログラム上認識させ、位置をアップデートする、というところまでなので、(当然ですが)なにかしらのモデル(メッシュ)を配置してあげないと動いていることが確認できないので、適当に好きなモデルなどを配置してください。

コンポーネントの概念

最後に少しだけコンポーネントの概念をメモっておきます。

最初、Unityのコンポーネントと同義だと思っていたので色々「?」になる状況があったのですが、少しだけUnityとは概念が違うようです。

大きな違いは、コンポーネントも階層構造を持てるということ。
Unityでは普通、GameObjectひとつに対してコンポーネントを追加していくように作成していくかと思います。

しかし、コンポーネント自体はネスト構造を持つことはできません。
もし持たせたい場合は、新しくGameObjectを作り、GameObject自体の入れ子を元にコンポーネントを配置していくことになるかと思います。

UEではコンポーネント自体にネスト構造を持たせることができるため、(自分のイメージでは)Unityのコンポーネントよりもより実体に近い形でインスタンス化されるもの、という認識です。

誤解を恐れずに言うと、UnityのGameObjectに、なにかひとつのコンポーネントを付けた(機能を持った)オブジェクト=UEのコンポーネント、という感じです。

こう把握してから一気に色々と理解が進んだのでメモとして残しておきます。

ハマった点

VRモードで動かすまでにいくつかハマった点があったのでメモ。

VRカメラの位置が指定した位置から開始しない

VRコンテンツ制作がメインなので、当然UEもそれを目的として始めました。
最初のごく簡単なプロジェクトを作成して、いざVRモードで見てみたらなぜかシーンビューのカメラ位置から開始されるという問題が。

結論から言うと、PawnのプロパティであるAuto Process PlayerDisableからPlayer0に変更することで直りました。
UEのドキュメントに載っているやつと載ってないのがあったので、地味にハマりました・・・。

便利拡張

UnityでC#書いているときは、///でコメントを、XMLのフォーマットで自動挿入してくれていたんですが、C++だとそれができないなーと思っていたら、拡張でまったく同じことをしてくれるものがありました。

jyn.jp


余談

ここからはVRコンテンツ開発とは全然関係なくなるので、完全に余談です。
公開されているUnreal Engine自体のソースコードからビルドしたエディタを使おうとして少しだけハマった点をメモ。

UE4を使うメリットのひとつとして、エンジンのソースコードが公開されていることが上げられます。
エンジンのソースコードを読むと、中でどういった処理がされているか、といったことを学ぶのに役立ちます。
またそれを見ておけば、もしエンジンのバグに遭遇したときにもそれを対処する方法が見つかるかもしれません。

ということで、ソースコードからビルドしてUE4を使ってみようと思ってやったところ、いくつか(環境依存の)問題が出てきたので、それをメモとして残しておこうと思います。

Visual Studioのセットアップでエラー

最初、UEをビルドするに当たって、まずプロジェクトファイルを生成する必要があります。
しかし、そのセットアップ用のバッチファイルを実行したところ、以下のようなエラーが表示されました。

ERROR: No 32-bit compiler toolchain found in C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\cl.exe

調べていくと、Visual C++ Toolsがインストールされていないことが原因のよう。

f:id:edo_m18:20180204165901p:plain

ということで、さっそくインストール・・・と思ったら、セットアップ時にクラッシュするじゃありませんか。

まずはクラッシュした理由をさぐるべく、クラッシュログの場所を探す。
場所はMSDNFAQ:Where are the setup logs stored ?)に掲載されていました。

場所は%Temp%とのこと。
(自分の環境だとC:\Users\[USER_NAME]\AppData\Local\Temp

似たような質問も上がっていました。

Setup detected an issue during the operation. Please click below to check for a solution and help us to improve the setup experience. | Microsoft Developer

フォントが悪さをしていた

色々調べていくと、どうやらフォントが悪さをしているということのよう。
どうも、VSはフォントと相性が悪いらしく、Windows Presentation Foundation Font Cacheというサービスが起動しているとクラッシュするらしい。

ということで、さっそくそれを停止してみたものの、セットアップでのクラッシュは直らず。
どこかで見かけた(記事のURLがわからないんですが・・)、フォントをいったん別の場所にバックアップしておき、消せるやつだけすべて消してからやるとうまくいくよ、というのを思い出してやってみたところ、うまく行きました。

なので、もし似た現象で困っている人がいたら、フォントを消せるだけすべて消してみて(いくつかはワーニングが出て消せない)リトライしてみるとうまく行くかもしれません。

参考にした記事

qiita.com

docs.unrealengine.com

docs.unrealengine.com

UnityのCompute ShaderでCurl Noiseを実装(流体編)

概要

今回は「流体編」と題しました。
というのも、発表されたカールノイズ自体には剛体との当たり判定を行う部分の記載もあるのですが、そこがうまく動いてないのでまずは流体部分の説明にフォーカスして書いていこうと思います。


Curl Noice。なんとなく流体っぽく見えるノイズ。
Curlとは「カール」、つまり「渦巻く」という意味からつけられた名前だそう。

(自分の浅い知識ながら)もう少し具体的に言うと、ベクトル場を回転(Curl)させることで作るノイズ、ということで「Curl Noise」というと思っています。

いつもお世話になっている物理のかぎしっぽさんで、この「ベクトル場の回転」についての記載があります。

ちなみに今回の主な実装は以下の記事を参考にさせていただきました。

qiita.com

また、発表された論文はこちら(PDF)

ソースコード

今回のサンプルはGithub上にUnityプロジェクトとしてアップしてあります。興味がある方はDLして見てみてください。

github.com

(自分の顔アイコンになっちゃうのどうにかならんか・・)

今回の記事で書くこと

今回の記事は、主にCurl Noiseについて触れます。
サンプルはComputeShaderを使ってUnityで実装しました。ComputeShaderによる実装についても少し触れます。

ただ、ComputeShaderなどの使い方などは知ってる前提であまり詳しくは解説しません。
ComputeShader自体については前の記事を読むなどしてください。

大まかな解説

自分はあまり物理に明るくありませんが、今回の実装を通して色々と学びがあったので、それをまとめる意味でも少しだけ解説を書きたいと思います。
(注) あくまで個人的な理解なので、間違っている可能性があります。

非圧縮性を確保する

流体が流体っぽく見える所以は、この非圧縮性が必要だという認識です。
非圧縮性の説明をWikipediaから引用させてもらうと、

非圧縮性流れ(ひあっしゅくせいながれ)とは流体力学において、流体粒子の内部で密度が一定の流体である。縮まない流体とも呼ばれる[1][2]。連続体力学における非圧縮性の概念を流体に適用したものである。
言い換えると、非圧縮性とは流体の速度の発散が 0 になることである(この表現が等価である理由は後述)。

とのこと。
ここで、流体の速度の発散が0になることであるとあります。

物理のかぎしっぽさんの記事のdivから、「発散」についての説明を引用させてもらうと、

スカラー場の勾配を考えたとき,ベクトル微分演算子  \nabla = \left(\frac{\partial}{\partial x_{1}}, \frac{\partial}{\partial x_{2}}, \frac{\partial}{\partial x_{3}}\right)というものを導入しました.そして,この  \nablaスカラー関数に作用させたものを勾配(  {\rm grad} )と呼びました.  \nabla をベクトル関数と内積を取る形で作用させたものを 発散 と呼びます.英語で発散を divergence と言うので,記号  {\rm div} を使う場合もあります.

さらに別の箇所から引用すると、

ベクトル場を,水(非圧縮流体)の流れだと考えると状況がイメージしやすいでしょう.この直方体領域は流れの中に置かれていますから,絶えず水が流れ込んだり出て行ったりしています.しかし,水は非圧縮流体だと仮定していますので,普通なら,入ってくる水量と出て行く水量は同じはずです.

と書かれています。
つまり、(自分の理解では)この「発散が0になる」とは、流れの中で流入と流出が等しく起こる=差分がゼロ、ということだと思います。

そしてカールノイズはこの「発散0を実現してくれるための手法」ということのようです。
理由として自分が理解しているのは、以下のように考えています。
まず言葉で説明すると、

あるベクトル場から得たベクトルを「回転」させ、「内積」を取った結果がゼロである。

ということ。式にすると、


\nabla \cdot \nabla \times \vec{v} ≡ 0

カール(回転)は、どうやら90°回転させることのようです。
つまり、「とあるベクトルを90°回転させたベクトルとの内積=0」ということです。
そもそも内積は、垂直なベクトルとの計算は0になりますよね。

なので、ある意味0になるのは自明だった、というわけです。(という認識です)

発散の定義が、 \nablaとの内積を取ったものが 0になる、ということなので、「だったら最初からゼロになるように回転しておこうぜ」というのが発想の基点なのかなと理解しています。

実際にUnity上で実行した動画はこんな感じ↓

パーティクルの位置の計算についてはComputeShaderを使って計算しています。
ComputeShaderについて前回2回に分けて記事を書いているので、よかったら見てみてください。

edom18.hateblo.jp

edom18.hateblo.jp

それから、描画周りの実装については、凹みTipsで過去に凹みさんが書かれていた以下の記事を参考に実装しています。
(というかほぼそのままです)
いつもありがとうございます。

tips.hecomi.com

用語解説

いくつか数学・物理的なワードが出てくるので、簡単にまとめておきます。

ベクトル場

ものすごくざっくりいうと、「ベクトル」がちらばった空間です。
分かりやすい例で言うと、風速ベクトルを空間にマッピングしたもの、と考えるといいかもしれません。

風速はその場所その場所で当然向きや強さが違います。
そしてその瞬間の風速ベクトルを記録したのがベクトル場、という感じです。

プログラム的に言うと、ベクトルを引数に取ってベクトルを返す関数、と見ることが出来ます。 (厳密に言うと風速は時間によっても変化するので、時間と位置による関数となります)

float3 windVec = GetWind(pos, time);

みたいな感じですね。
今回のCurlNoiseについては時間的変化は考えず、以下のように速度ベクトルを求めています。

float3 x = _Particles[id].position;
float3 velocity = CurlNoise(x);

ベクトル場については、物理のかぎしっぽさんのところにとても分かりやすい例と図があるので、そちらを見てみてください。

カール(Curl・rot)

ベクトル場の回転を表す。
前述の、物理のかぎしっぽさんの記事から引用させてもらうと、

三次元ベクトルの場合,ナブラを 外積を取る形で作用させる ことも可能です.これを 回転 と呼び,  \nabla \times, rot もしくは curl という記号で表現します.この記事では,回転について基本的な意味や性質を考えます.

ramp関数

ramp関数 | Wikipediaとは、グラフが斜路(ramp)に見えることから名付けられたそう。

論文で書かれている式をグラフにしてみると以下の形になります。
なんとなく「斜路」という意味が分かりますね。

f:id:edo_m18:20180117234619p:plain

本題

さて、ここからが、今回実装した内容の解説です。
まず、Curl Noiseについてざっと解説してみます。(自分もまだ理解が浅いですが)

前述のように、ベクトル場を(ベクトル解析でいう)回転させることによって得られるノイズを「Curl Noise」と呼んでいるようです。
元々は、アーティスティックな流体表現を実現するために考案されたもののようです。

物理のかぎしっぽさんの記事を参考に式を表すと以下のようになります。

f:id:edo_m18:20171014165055p:plain

 \nablaは「ナブラ」と読み、「微分演算子をベクトルに組み合わせたナブラというベクトル演算子」ということのようです。

記号を見ると偏微分の記号なので、要は、ベクトル場の各ベクトルに対してちょっとずつ位置をずらして計算したもの、と見ることができます。

CurlNoiseを生成している関数を見てみると、以下のようになります。

float3 CurlNoise(Particle p)
{
    const float e = 0.0009765625;
    const float e2 = 2.0 * e;
    const float invE2 = 1.0 / e2;

    const float3 dx = float3(e, 0.0, 0.0);
    const float3 dy = float3(0.0, e, 0.0);
    const float3 dz = float3(0.0, 0.0, e);

    float3 pos = p.position;

    float3 p_x0 = SamplePotential(pos - dx, p.time);
    float3 p_x1 = SamplePotential(pos + dx, p.time);
    float3 p_y0 = SamplePotential(pos - dy, p.time);
    float3 p_y1 = SamplePotential(pos + dy, p.time);
    float3 p_z0 = SamplePotential(pos - dz, p.time);
    float3 p_z1 = SamplePotential(pos + dz, p.time);

    float x = (p_y1.z - p_y0.z) - (p_z1.y - p_z0.y);
    float y = (p_z1.x - p_z0.x) - (p_x1.z - p_x0.z);
    float z = (p_x1.y - p_x0.y) - (p_y1.x - p_y0.x);

    return float3(x, y, z) * invE2;
}

eが、「ちょびっと動かす」部分の値で、それを各軸に対して少しずつ変化させる量としてdxdydzを定義しています。
そしてそれぞれ変化した値をSamplePotential関数に渡し、さらにそれの差分を取ることで計算を行っています。

ベクトル場の回転について、論文に書かれているものは以下です。


\vec{v}(x, y, z) =
\left(
\frac{∂\psi_3}{∂ y} − \frac{∂\psi_2}{∂ z}, \frac{∂\psi_1}{∂ z} − \frac{∂\psi_3}{∂ x}, \frac{∂\psi_2}{∂ x} − \frac{∂\psi_1}{∂ y}
\right)

ベクトル微分演算子を作用させるので、微分を取る目的で以下のように計算しています。

float3 p_x0 = SamplePotential(pos - dx, p.time);
float3 p_x1 = SamplePotential(pos + dx, p.time);
float3 p_y0 = SamplePotential(pos - dy, p.time);
float3 p_y1 = SamplePotential(pos + dy, p.time);
float3 p_z0 = SamplePotential(pos - dz, p.time);
float3 p_z1 = SamplePotential(pos + dz, p.time);

それぞれの方向(x, y, z)に対してちょっとずつずらした値を取得しているわけですね。
そして続く計算で、前述の「ベクトル場の回転」を計算します。

float x = (p_y1.z - p_y0.z) - (p_z1.y - p_z0.y);
float y = (p_z1.x - p_z0.x) - (p_x1.z - p_x0.z);
float z = (p_x1.y - p_x0.y) - (p_y1.x - p_y0.x);

[2018.06.29追記]
すみません、計算ミスをしていました。以前は以下のように書いていましたが、最後のパラメータの微分の計算が「マイナス」になるのが正解です。

// 間違っていた記述
float x = (p_y1.z - p_y0.z) - (p_z1.y + p_z0.y);
float y = (p_z1.x - p_z0.x) - (p_x1.z + p_x0.z);
float z = (p_x1.y - p_x0.y) - (p_y1.x + p_y0.x);

↑カッコでくくった右側の計算が「プラス」になっていました。

前述の数式と見比べてもらうと同じことを行っているのが分かるかと思います。

誤解を恐れずに言えば、カールノイズのコア部分はここだけです。
つまり、

  1. ベクトル場からベクトルを得る
  2. 得たベクトルを回転させる
  3. 得たベクトルを速度ベクトルとして計算する
  4. 速度ベクトルから次の位置を決定する

だけですね。

あとはこの、算出された回転済みのベクトル(速度)をパーティクルに与えるだけで、なんとなく流体の流れっぽい動きをしてくれるようになります。

ノイズを生成する

さて、カールノイズをもう少し具体的に見ていきます。
カール「ノイズ」というくらいなので、当然ノイズを生成する必要があります。

色々なカールノイズのサンプルを見ると、シンプレクスノイズかパーリンノイズを利用しているケースがほとんどです。
論文でもパーリンノイズに言及しているので、今回はパーリンノイズを利用してカールノイズを作成しました。

ノイズを生成する上で、論文には以下のように書かれています。

To construct a randomly varying velocity field we use Perlin noise  N(\vec{x}) in our potential. In 2D, ψ = N. In 3D we need three components for the potential: three apparently uncorrelated noise functions (a vector  vec{N}(\vec{x}) do the job, which in practice can be the same noise function evaluated at large offsets.

Note that if the noise function is based on the integer lattice and smoothly varies in the range [−1,1], then the partial derivatives of the scaled N(x/L) will vary over a length-scale L with values approximately in the range O([−1/L,1/L]). This means we can expect vortices of diameter approximately L and speeds up to approximately O(1/L): the user may use this to scale the magnitude of ψ to get a desired speed.

ざっくり訳してみると、


ランダムな変化するベクトル場を構築するのに、パーリンノイズ( \vec{x})をポテンシャルのために利用する。2Dの場合は「 \psi = N」。

3Dの場合はポテンシャルのために3つの要素が必要となる。
3つの、明らかに無関係なノイズ関数( \vec{N}(\vec{x})を使用する。それらは同じノイズ関数を、大きなオフセットを持たせたもの。

Note: もし、ノイズ関数が整数格子にもとづいており、[-1, 1]の範囲で滑らかに変化するのであれば、 N(x/L)にスケーリングされた偏微分は、おおよそO([−1/L,1/L])の範囲の値でスケールLに渡って変化する。

これは、ほぼ直径Lの渦とO(1/L)までの速度を期待することができることを意味する。ユーザは \psiの値にスケールされた(望みの)スピードを使えるかもしれない。


ということで、ノイズとスケールを考慮した処理は以下のようになります。

float3 SamplePotential(float3 pos)
{
    float3 s = pos / _NoiseScales[0];
    return Pnoise(s);
}

_NoiseSclaesは、いくつかのスケール値が入った配列です。
が、今回はひとつのみ利用しているので_NoiseScales[0]だけを使っています。

Pnoiseはパーリンノイズを利用したベクトル場の計算です。
実装は論文に書かれているように、大きなオフセットを持たせた同一のパーリンノイズ関数を利用して3要素を計算しています。

// パーリンノイズによるベクトル場
// 3Dとして3要素を計算。
// それぞれのノイズは明らかに違う(極端に大きなオフセット)を持たせた値とする
float3 Pnoise(float3 vec)
{
    float x = PerlinNoise(vec);

    float y = PerlinNoise(float3(
        vec.y + 31.416,
        vec.z - 47.853,
        vec.x + 12.793
        ));

    float z = PerlinNoise(float3(
        vec.z - 233.145,
        vec.x - 113.408,
        vec.y - 185.31
        ));

    return float3(x, y, z);
}

ちなみにPerlinNoiseは以前、JavaScriptで実装したときのものをCompute Shaderに移植したもので、以下のように実装しています。

float PerlinNoise(float3 vec)
{
    float result = 0;
    float amp = 1.0;

    for (int i = 0; i < _Octaves; i++)
    {
        result += Noise(vec) * amp;
        vec *= 2.0;
        amp *= 0.5;
    }

    return result;
}

float Noise(float3 vec)
{
    int X = (int)floor(vec.x) & 255;
    int Y = (int)floor(vec.y) & 255;
    int Z = (int)floor(vec.z) & 255;

    vec.x -= floor(vec.x);
    vec.y -= floor(vec.y);
    vec.z -= floor(vec.z);

    float u = Fade(vec.x);
    float v = Fade(vec.y);
    float w = Fade(vec.z);

    int A, AA, AB, B, BA, BB;

    A = _P[X + 0] + Y; AA = _P[A] + Z; AB = _P[A + 1] + Z;
    B = _P[X + 1] + Y; BA = _P[B] + Z; BB = _P[B + 1] + Z;

    return Lerp(w, Lerp(v, Lerp(u, Grad(_P[AA + 0], vec.x + 0, vec.y + 0, vec.z + 0),
                                    Grad(_P[BA + 0], vec.x - 1, vec.y + 0, vec.z + 0)),
                            Lerp(u, Grad(_P[AB + 0], vec.x + 0, vec.y - 1, vec.z + 0),
                                    Grad(_P[BB + 0], vec.x - 1, vec.y - 1, vec.z + 0))),
                    Lerp(v, Lerp(u, Grad(_P[AA + 1], vec.x + 0, vec.y + 0, vec.z - 1),
                                    Grad(_P[BA + 1], vec.x - 1, vec.y + 0, vec.z - 1)),
                            Lerp(u, Grad(_P[AB + 1], vec.x + 0, vec.y - 1, vec.z - 1),
                                    Grad(_P[BB + 1], vec.x - 1, vec.y - 1, vec.z - 1))));
}

パーリンノイズ自体の詳しい内容は、調べるとたくさん記事が出てくるのでそちらをご覧ください。

カールノイズのカーネル

さて最後に、カールノイズを計算しているカーネル(Compute Shaderのmain関数のようなもの)を見てみます。

[numthreads(8, 1, 1)]
void CurlNoiseMain(uint id : SV_DispatchThreadID)
{
    float3 pos = _Particles[id].position;

    float3 velocity = CurlNoise(_Particles[id]);

    _Particles[id].velocity = velocity * _SpeedFactor * _CurlNoiseIntencity;
    _Particles[id].position += _Particles[id].velocity * _DeltaTime;

    _Particles[id].time += _DeltaTime;
    float scale = 1.0 - (_Particles[id].time / _Particles[id].lifeTime);
    if (scale < 0)
    {
        _Particles[id].active = false;
        return;
    }

    _Particles[id].scale = scale;
}

_Particlesがパーティクルの配列になっていて、Particle構造体がそれぞれのパーティクルの位置や速度などを保持しています。
それを、毎フレームごとに更新し、それをレンダリングすることで流体のような動きを実現している、というわけです。

現在リポジトリにアップしているものは、これに加えて、剛体との衝突判定の実装を試みているのでもう少し複雑な状態になっていますが、衝突判定なしで流体のような動きだけがほしい場合は以上の実装で実現することができます。

ガチの流体計算に比べるとだいぶ簡単な処理で実現できているのが分かってもらえたかと思います。

次回予告 - 衝突判定

さて、冒頭でも書きましたが、このカールノイズ。
簡単な衝突判定を利用して、球体に沿わしたり、といったことができます。

球体が干渉しているのが分かるかと思います。
衝突判定が取れるようになれば、さらに流体感を増した表現になるので、ぜひマスターしたいところです。