なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

【Unity】JobSystemのサンプル作成と計測

始めに

並列処理が出来るJobSystemのサンプルを作成して、どれほどの性能なのかを計ってみようと思います。

何も考えずにサンプルを作って動作確認しているので、JobSystemについての詳細については一切ふれません。

もし、JobSystemについて詳しく知りたい方はUnity公式の動画がおすすめです。


www.youtube.com


www.youtube.com


作成するサンプル

指定された立方体内を反射し続けるCubeを大量に作って、以下の環境でどれだけの性能が出るかを計測しました。

  • 愚直な実装 + MeshRenderer
  • JobSystem + Burst + MeshRenderer
  • JobSystem + Burst + RenderMeshInstanced

また、負荷を高めるために O(n2) の計算を加えています。

ここで載せるコードは雑に書いたものになるので、参考程度にして下さい。

動作環境

  • Core i7 8700K
  • Unity 2021.3.8f1 URP


愚直な実装 + MeshRenderer

MonobehaviorのUpdateにそのままCubeの移動処理を書きます。

CubeMove.cs

using System.Collections.Generic;
using UnityEngine;

public class CubeMove : MonoBehaviour
{
    [SerializeField] private int _instanceCount;
    [SerializeField] private GameObject _prefab;
    [SerializeField] private float _range;

    [SerializeField] private float _velocity;
    
    private readonly List<ObjData> _objDatas = new();
    private readonly List<GameObject> _gameObjects = new();
    
    private class ObjData
    {
        public Vector3 Position;
        public Vector3 Velocity;
    }

    private void Start()
    {
        for (int i = 0; i < _instanceCount; ++i)
        {
            var position = RandomPositionInRange();
            var instance = Instantiate(_prefab, position, Quaternion.identity);
            _objDatas.Add(new ObjData { Position = position, Velocity = RandomDirectionVelocity() } );
            _gameObjects.Add(instance);
        }
    }

    private void Update()
    {
        UpdateObjects();
    }

    private void UpdateObjects()
    {
        for (int i = 0; i < _instanceCount; ++i)
        {
            SumVelocity(); // MEMO: わざと負荷を上げる
            
            var obj = _objDatas[i];

            var deltaTime = Time.deltaTime;
            var diff = obj.Velocity * deltaTime;
            
            if (Mathf.Abs(obj.Position.x + diff.x) >= _range) obj.Velocity.x *= -1;
            if (Mathf.Abs(obj.Position.y + diff.y) >= _range) obj.Velocity.y *= -1;
            if (Mathf.Abs(obj.Position.z + diff.z) >= _range) obj.Velocity.z *= -1;

            obj.Position += obj.Velocity * deltaTime;

            _gameObjects[i].transform.position = obj.Position;
        }
    }

    private void SumVelocity()
    {
        var sumVelocity = Vector3.zero;
        var sumPosition = Vector3.zero;

        for (int i = 0; i < _objDatas.Count; ++i)
        {
            sumVelocity += _objDatas[i].Velocity;
            sumPosition += _objDatas[i].Position;
        }
    }
    
    private Vector3 RandomPositionInRange()
    {
        return new Vector3(
            Random.Range(-_range, _range),
            Random.Range(-_range, _range),
            Random.Range(-_range, _range)
        );
    }

    private Vector3 RandomDirectionVelocity()
    {
        return Random.onUnitSphere * _velocity;
    }
}


こちらの実行結果が次のようになります。

cube数が100個の場合は約100fps、1000個の場合は約13fpsになります。

n = 100 n = 1000


JobSystem + Burst + MeshRenderer

次にJobSystemとBurstを使ってサンプルを作ってみます。

CubeMoveByJobSystem.cs

using System.Collections.Generic;
using System.Linq;
using Unity.Burst;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Jobs;
using Random = UnityEngine.Random;

public class CubeMoveByJobSystem : MonoBehaviour
{
    [SerializeField] private int _instanceCount;
    [SerializeField] private GameObject _prefab;
    [SerializeField] private float _range;

    [SerializeField] private float _velocity;
    
    private ObjData[] _objDatas;
    private readonly List<GameObject> _gameObjects = new();
    private TransformAccessArray _transformAccessArray;

    private struct ObjData
    {
        public Vector3 Position;
        public Vector3 Velocity;
    }

    private void Start()
    {
        _objDatas = new ObjData[_instanceCount];

        var objDataList = new List<ObjData>();
        for (int i = 0; i < _instanceCount; ++i)
        {
            var position = RandomPositionInRange();
            var instance = Instantiate(_prefab, position, Quaternion.identity);
            objDataList.Add(new ObjData { Position = position, Velocity = RandomDirectionVelocity() } );
            _gameObjects.Add(instance);
        }

        _objDatas = objDataList.ToArray();
        _transformAccessArray = new TransformAccessArray(_gameObjects.Select(obj => obj.transform).ToArray());
    }

    private void Update()
    {
        UpdateByJobSystem();
    }

    private void UpdateByJobSystem()
    {
        var objDataArray = new NativeArray<ObjData>(_instanceCount, Allocator.TempJob);
        var objDataReadArray = new NativeArray<ObjData>(_instanceCount, Allocator.TempJob);
        
        objDataArray.CopyFrom(_objDatas);
        objDataReadArray.CopyFrom(_objDatas);
        
        var job = new MoveObjectJob
        {
            objDatas = objDataArray,
            objDatasRead = objDataReadArray,
            range = _range,
            deltaTime = Time.deltaTime
        };

        var handler = job.Schedule(_transformAccessArray);
        handler.Complete();
        
        objDataArray.CopyTo(_objDatas);

        objDataArray.Dispose();
        objDataReadArray.Dispose();
    }

    private Vector3 RandomPositionInRange()
    {
        return new Vector3(
            Random.Range(-_range, _range),
            Random.Range(-_range, _range),
            Random.Range(-_range, _range)
        );
    }

    private Vector3 RandomDirectionVelocity()
    {
        return Random.onUnitSphere * _velocity;
    }

    private void OnDestroy()
    {
        _transformAccessArray.Dispose();
    }

    [BurstCompile]
    private struct MoveObjectJob : IJobParallelForTransform
    {
        public NativeArray<ObjData> objDatas;
        [ReadOnly] public NativeArray<ObjData> objDatasRead;
        [ReadOnly] public float range;
        [ReadOnly] public float deltaTime;

        public void Execute(int index, TransformAccess transform)
        {
            SumVelocity(); // MEMO: わざと負荷を上げる
            
            var obj = objDatas[index];

            var diff = obj.Velocity * deltaTime;
            
            if (Mathf.Abs(obj.Position.x + diff.x) >= range) obj.Velocity.x *= -1;
            if (Mathf.Abs(obj.Position.y + diff.y) >= range) obj.Velocity.y *= -1;
            if (Mathf.Abs(obj.Position.z + diff.z) >= range) obj.Velocity.z *= -1;

            obj.Position += obj.Velocity * deltaTime;
            objDatas[index] = obj;

            transform.localPosition = obj.Position;
        }
        
        private void SumVelocity()
        {
            var sumVelocity = Vector3.zero;
            var sumPosition = Vector3.zero;

            for (int i = 0; i < objDatasRead.Length; ++i)
            {
                sumVelocity += objDatasRead[i].Velocity;
                sumPosition += objDatasRead[i].Position;
            }
        }
    }
}


こちらの実行結果は次のようになります。

n = 1000の場合は安定して80fps近く出せてますが、n = 5000で40fps行かないぐらい、n = 10000で約15fpsまで落ちました。

n = 1000 n = 5000 n = 10000


JobSystem + Burst + RenderMeshInstanced

先程のサンプルの処理速度を見るに、計算処理ではなく描画処理がボトルネックになっているとわかりました。(計算負荷を上げている個所をコメントアウトしてもフレームレートが変わらない)

なので、MeshRendererではなく、Graphics.RenderMeshInstancedを使って描画をしてみます。( Unity - Scripting API: Graphics.RenderMeshInstanced

CubeMoveByJobySystemAndGpuInstancing.cs

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Rendering;
using Random = UnityEngine.Random;

public class CubeMoveByJobySystemAndGpuInstancing : MonoBehaviour
{
    [SerializeField] private int _instanceCount;
    [SerializeField] private float _range;
    [SerializeField] private Mesh _mesh;
    [SerializeField] private Material _material;

    [SerializeField] private float _velocity;
    
    private ObjData[] _objDatas;
    private RenderParams _renderParams;

    private const int InstanceCountPerDraw = 1023;

    private struct ObjData
    {
        public Vector3 Position;
        public Vector3 Velocity;
    }

    private void Start()
    {
        _objDatas = new ObjData[_instanceCount];

        var objDataList = new List<ObjData>();
        for (int i = 0; i < _instanceCount; ++i)
        {
            var position = RandomPositionInRange();
            objDataList.Add(new ObjData { Position = position, Velocity = RandomDirectionVelocity() } );
        }

        _objDatas = objDataList.ToArray();
        _renderParams = new RenderParams(_material) { receiveShadows = true, shadowCastingMode = ShadowCastingMode.On };
    }

    private void Update()
    {
        UpdateByJobSystem();
    }

    private void UpdateByJobSystem()
    {
        var objDataArray = new NativeArray<ObjData>(_instanceCount, Allocator.TempJob);
        var objDataReadArray = new NativeArray<ObjData>(_instanceCount, Allocator.TempJob);
        var objMatrix = new NativeArray<Matrix4x4>(_instanceCount, Allocator.TempJob);

        objDataArray.CopyFrom(_objDatas);
        objDataReadArray.CopyFrom(_objDatas);

        var job = new MoveObjectJob
        {
            objDatas = objDataArray,
            objDatasRead = objDataReadArray,
            objMatrix = objMatrix,
            range = _range,
            deltaTime = Time.deltaTime
        };

        var handler = job.Schedule(_instanceCount, 0);
        handler.Complete();
        
        objDataArray.CopyTo(_objDatas);

        DrawAll(objMatrix);
        
        objDataArray.Dispose();
        objDataReadArray.Dispose();
        objMatrix.Dispose();
    }

    private Vector3 RandomPositionInRange()
    {
        return new Vector3(
            Random.Range(-_range, _range),
            Random.Range(-_range, _range),
            Random.Range(-_range, _range)
        );
    }

    private Vector3 RandomDirectionVelocity()
    {
        return Random.onUnitSphere * _velocity;
    }

    private void DrawAll(NativeArray<Matrix4x4> matricesArray)
    {
        for (int i = 0; i < _instanceCount; i += InstanceCountPerDraw)
        {
            var length = Mathf.Min(InstanceCountPerDraw, _instanceCount - i);
            Graphics.RenderMeshInstanced(_renderParams, _mesh, 0, matricesArray, length, i);
        }
    }

    [BurstCompile]
    private struct MoveObjectJob : IJobParallelFor
    {
        public NativeArray<ObjData> objDatas;
        [ReadOnly] public NativeArray<ObjData> objDatasRead;
        [ReadOnly] public float range;
        [ReadOnly] public float deltaTime;
        [WriteOnly] public NativeArray<Matrix4x4> objMatrix;

        public void Execute(int index)
        {
            SumVelocity(); // MEMO: わざと負荷を上げる
            
            var obj = objDatas[index];

            var diff = obj.Velocity * deltaTime;
            
            if (Mathf.Abs(obj.Position.x + diff.x) >= range) obj.Velocity.x *= -1;
            if (Mathf.Abs(obj.Position.y + diff.y) >= range) obj.Velocity.y *= -1;
            if (Mathf.Abs(obj.Position.z + diff.z) >= range) obj.Velocity.z *= -1;

            obj.Position += obj.Velocity * deltaTime;
            objDatas[index] = obj;

            objMatrix[index] = Matrix4x4.TRS(obj.Position, Quaternion.identity, Vector3.one);
        }
        
        private void SumVelocity()
        {
            var sumVelocity = Vector3.zero;
            var sumPosition = Vector3.zero;

            for (int i = 0; i < objDatasRead.Length; ++i)
            {
                sumVelocity += objDatasRead[i].Velocity;
                sumPosition += objDatasRead[i].Position;
            }
        }
    }
}


実行結果は次のとおりです。(マテリアルを設定する必要があるので、Cubeの色が変わっています)

n = 5000 n = 10000 n = 20000

n = 5000では安定して約90fps、n = 10000でも安定して50fps以上でました。

ただし、n = 20000まで来ると約20fpsになります。


計測結果 まとめ

n = 1000 n = 5000 n = 10000
愚直な実装 + MeshRenderer 13fps - -
JobSystem + Burst + MeshRenderer 80fps 40fps 15fps
JobSystem + Burst + RenderMeshInstanced 140fps 90fps 50fps



まとめ・感想

JobSystemで簡単なサンプルを作ってみましたが、とても取っつきやすい印象でした。実行速度もなかなか良い結果が出たので実用レベルと感じてみます。

また、Graphics.RenderMeshInstancedはNativeArrayをそのまま渡せるので、JobSystemと相性が良いんじゃないかなと思います。

 Graphics.RenderMeshInstanced(_renderParams, _mesh, 0, matricesArray);


計測結果から、極限のパフォーマンスを目指すとき以外はJobSystemでも事足りると感じています。(JobSystemを使うときは極限のパフォーマンスを目指すときかもしれませんが....)

ComputeShaderと比べて、学習コストの低さやC#でコードが書けること、Profilerで実行速度を観測できる点でJobSystemの方が圧倒的に扱いやすいです。


参考

shibuya24.info

shibuya24.info


www.youtube.com

【Boidsアルゴリズム】球体の障害物を回避させるアルゴリズムを考える

目次

始めに

過去のBoidsに関する記事です。Boidsに関してはここではあまり触れないのでご了承ください。

過去記事



障害物から"逃げる"

始めに障害物から逃げる実装について簡単にお話します。

  1. 障害物の中心座標から個体へのベクトル  \vec{p} を求めて、ベクトルの長さが障害物から逃げる範囲の半径  r_e 以下であるかをチェック
  2. 個体が範囲内である場合はベクトル  \vec{p} を使って斥力を求めて、個体の速度ベクトルに加算する


結果

こちらを実装した結果が次のgifになります。見て分かる通り、この実装だけでは"避ける"ではなく"逃げる"となり、障害物へ向かってくる個体はそのままぶつかって跳ね返る挙動をします。


この方法でパラメータを調整したり、斥力を距離に反比例するようにしても避けるような挙動を作るのは難しかったです。



障害物を"回避する"

"逃げる"とは別に"回避する"をBoidsに実装します。

障害物を回避する方法は次のようになります。

  1. 障害物を回避する範囲内にいる、かつ逃げる範囲外にいるかチェック
  2. 前方向に障害物があるかをチェック
  3. 個体の速度ベクトル  \vec{v} を延長して、障害物の中心点との距離  d を求めて障害物から逃げる範囲の半径  r_e 以下であるかをチェック
  4. 1~3の条件を満たした個体の速度ベクトル  \vec{v} と、個体から障害物の中心点へのベクトル  \vec{o} との外積を計算して回転軸を求める。
  5. 回転軸と各速度を組み合わせて、速度ベクトルを回転させる。


1. 障害物を回避する範囲内にいる、かつ逃げる範囲外にいるか

こちらは個体から障害物への距離  d を求めて、それが  r_e <  d <=  r_a であるかをチェックします。( r_e : 逃げる範囲、 r_a : 回避範囲 )範囲外の個体は回避処理は適用させません。


2. 前方向に障害物があるかをチェック

個体の速度ベクトル  \vec{v} と個体から障害物へのベクトル  \vec{o} との内積  dot(\vec{v}, \vec{o}) を求めます。(内積を求める前に2つのベクトルを正規化する。) 内積が0以下の場合は  \vec{v} \vec{o} がなす角が鈍角になり、前方に障害物がない状態になります。なので、この場合は回避処理は行いません。


3. 個体の速度ベクトル  \vec{v} を延長して、障害物の中心点との距離  d を求めて障害物から逃げる範囲の半径  r_e 以下であるかをチェック

個体の速度ベクトルをまっすぐ延ばして直線を作ります。次に、障害物の中心点Oから直線に向けて垂直な線分OPを作って、その線分の距離  d を求めます。

この距離  d が逃げる範囲  r_e 以下であるかをチェックします。


こちらの計算は次のように行います。

  1. 個体の速度ベクトル  \vec{v} と 個体から障害物の中心点へのベクトル  \vec{o} との外積  cross(\vec{v}, \vec{o}) を求める
  2. 求めた外積のベクトルの長さ  length を計算して、速度ベクトルの長さ  |\vec{v}| で割って距離  dを求める
  3. 距離  d r_e 以下であるかチェックする

上記を1つの式にまとめますと次にようになります。


d =  \displaystyle\frac{ |cross(\vec{v}, \vec{o})| }{|\vec{v}|}

外積で求められるベクトルの大きさは2つのベクトルが生成する平行四辺形の面積になる性質があります。 (参考 : 「外積の長さ = 平行四辺形の面積」 証明  - 理数アラカルト -

次に底辺をなすベクトル  \vec{v} の大きさで割ることで平行四辺形の高さ = 中心点からの直線への距離を求められます。


4, 5. 回転軸を求めて速度ベクトルを回転させる

これまでの条件を満たした個体に対して障害物を避けるような回転を加えます。

回転軸は3.で求めた外積の単位ベクトル  \vec{A} になります。この回転軸と各速度を組み合わせて回転を生成します。

回転の生成ではロドリゲスの回転公式を使っています。(参考 : 3D数学の復習と実践(ロドリゲスの回転公式) - なおしのこれまで、これから


後は個体の速度ベクトルに回転を適用することで、障害物を避けるような挙動ができます。


コード

以上の実装はComputeShaderで行っております。 参考程度にして頂けると助かります。

inline float3x3 CalcAvoidObstacleTorque(const float3 position, const float3 velocity)
{
    const float3 diff = _ObstaclePosition.xyz - position;
    const float distance = sqrt(dot(diff, diff));
    
    if(distance > _AvoidObstacleRadius || _EscapeObstacleRadius > distance) // 障害物を避ける範囲内にいるか?
    {
        return float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
    }

    const float3 target2ObstacleDirection = normalize(diff);
    const float3 forward = normalize(velocity);
    const float directionDot = dot(target2ObstacleDirection, forward);

    if(directionDot < 0) // 前方向に障害物がないか
    {
        return float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
    }

    const float3 forward2DiffCross = cross(forward, diff);
    const float forward2ObstacleDistance = length(forward2DiffCross);

    if(forward2ObstacleDistance > _EscapeObstacleRadius) // 障害物から逃げる範囲と速度ベクトルの直線が重なっていないか
    {
        return float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
    }

    const float crossElementSum = forward2DiffCross.x + forward2DiffCross.y + forward2DiffCross.z;
    const float3 axis = crossElementSum != 0 ? normalize(forward2DiffCross) : float3(0, 1, 0);
    
    return CalcRotateMatrixByAxis(axis, _AvoidObstacleAngularVelocity);
}


....
....

const float3x3 avoidTorque = CalcAvoidObstacleTorque(boidData.position, velocity);
const float3 avoidVelocity = mul(velocity, avoidTorque);


結果

見やすいようにBoidsの行動範囲を2次元にしています。 分かれずに球体に向かっていく個体は障害物を上下方向に避けようとして詰まっています。


応用

障害物を複数追加して動作するようにした結果は次にようになりました。 (はっきりした方法ではないですが、複数の障害物を回避する場合は回転軸を合成しています。)


障害物で輪っかを作ったら真ん中の穴を通ってくれました。



考察

問題点

この方法で障害物を組み合わせて壁や窪みを作った場合は上手く避けてくれません。(回避先については考慮されていない) この方法で避けられるのは線状のものに限られるでしょう。

壁や窪みが静的なオブジェクトであれば前回のような力場を一度だけ計算して、衝突判定させるのが良いかもしれません。

shitakami.hatenablog.com


複数の障害物に対しての厳密な回避を考える

複数の障害物が接触している場合は、1つの球体に合成して回避する方法もありかもしれません。(しかし、障害物の穴などを塞いでしまうかも)

一番良いと考えているのが、速度ベクトルをずらして複数のRayを飛ばして障害物に当たらないor障害物との交点が最も遠い方向に逃げるようにするのが良いかもしれません。

この方法についてはこちらの動画がとても参考になると思います。

www.youtube.com



最後に

今回は前回できなかったBoidsの回避について実装と考察を行いました。

成果物としては自分の求める要件を満たすものになったので、考察で述べた内容についてはまた遠い将来になると思われます。

これからはこの機能を作りたいコンテンツで使っていく予定です。



参考

今回のBoidsの実装は「Unity Gracphics Programming vol.1」を元にしています。

UnityGraphicsProgramming-vol1.pdf - Google ドライブ


回避のアルゴリズムを考えるうえでこちらの本の計算幾何学の内容を参考にしています。

【Vive Pro Eye】アイトラッキングのデータを取得する場合はコールバックを使った方が良い

始めに

前回アイトラッキングの情報をアバターに反映させました。

shitakami.hatenablog.com


そのときにAnalyzerで処理を調べたところ、アイトラッキングの情報取得が重いことが分かりました。なので、取得処理をコールバックに変更してどれだけ軽くなるのかを調べてまとめようと思います。



アイトラッキングの情報の取得方法について

SRanipal_Eye_AIP.GetEyeData_v2

[DllImport("SRanipal")]
public static extern Error GetEyeData_v2(ref EyeData_v2 data);

このメソッドは引数に EyeData_v2 の参照を渡して、そこにトラッキング情報を保持させて結果を受け取れるメソッドです。

このメソッドを直接呼び出すことはほとんどなく、主に SRanipal_Eye_v2.Update() で呼び出されます。

private static bool UpdateData()
{
    if (Time.frameCount == LastUpdateFrame) return LastUpdateResult == Error.WORK;
    else LastUpdateFrame = Time.frameCount;
    LastUpdateResult = SRanipal_Eye_API.GetEyeData_v2(ref EyeData_);

    return LastUpdateResult == Error.WORK;
}


また、この UpdateData() はトラッキング情報の一部(目の開閉や瞳の位置など)を取得するときに呼び出されるようになっています。(ただし、同フレーム内で1度だけ呼ばれる)

public static bool GetEyeOpenness(EyeIndex eye, out float openness)
{
    UpdateData();
    return GetEyeOpenness(eye, out openness, EyeData_);
}



RegisterEyeDataCallback_v2

[DllImport("SRanipal")]
public static extern int RegisterEyeDataCallback_v2(IntPtr callback);

このメソッドは引数にコールバック関数のポインタを渡して、そのコールバック関数を経由してトラッキング情報を取得します。

また、このメソッドを直接呼ぶのではなくラッピングされたメソッドを経由して呼び出されます。

public static int WrapperRegisterEyeDataCallback(System.IntPtr callback)
{
    return SRanipal_Eye_API.RegisterEyeDataCallback_v2(callback);
}


サンプルでのこのメソッドの使用方法は次のようになっています。処理としては、コールバック関数を登録出来る場合は登録を行い、出来ない場合は登録を解除するとなっています。

注意すべき点として、コールバック関数は static 関数でなくてはいけません。(それ以外はUnityが落ちたりします)

アイトラッキングの情報を使用する場合は static 変数の eyeData を使って情報を取得します。

private static EyeData_v2 eyeData

private void Update()
{
    if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING &&
        SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) return;
    
    if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == true && eye_callback_registered == false)
    {
        SRanipal_Eye_v2.WrapperRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
        eye_callback_registered = true;
    }
    else if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback == false && eye_callback_registered == true)
    {
        SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic)EyeCallback));
        eye_callback_registered = false;
    }
}

private static void EyeCallback(ref EyeData_v2 eye_data)
{
    eyeData = eye_data;
}


また、注意としてコールバックを使用する場合は SRanipal_Eye_FrameworkEnable Eye Data Callback にチェックを入れる必要があります。

f:id:vxd-naoshi-19961205-maro:20220222010255p:plain



簡単なサンプルでの計測

SRanipal_Eye_AIP.GetEyeData_v2RegisterEyeDataCallback_v2 でそれぞれトラッキングの情報を取得して、負荷を計測してみます。

SRanipal_Eye_AIP.GetEyeData_v2

サンプルプログラムは次のようになります。

using UnityEngine;
using ViveSR.anipal.Eye;

public class ViveProEyeTrackingInput_GetByMethod : MonoBehaviour
{
    private EyeData_v2 _eyeData;

    private void Update()
    {
        var openness = GetEyeOpenness(EyeIndex.LEFT);
        Debug.Log(openness);
    }

    private float GetEyeOpenness(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetEyeOpenness(eyeIndex, out var openness);
        return openness;
    }
}


計測結果は次のようになります。

Update 処理自体で約5ms、SRanipal_Eye_AIP.GetEyeData_v2 だけで 4.67ms も使っています。

f:id:vxd-naoshi-19961205-maro:20220222004954p:plain



RegisterEyeDataCallback_v2

サンプルプログラムです。

using System.Runtime.InteropServices;
using UnityEngine;
using ViveSR.anipal.Eye;

public class ViveProEyeTrackingInput : MonoBehaviour
{
    private static EyeData_v2 _eyeData;
    private bool eye_callback_registered = false;

    private void Update()
    {
        if (SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.WORKING &&
            SRanipal_Eye_Framework.Status != SRanipal_Eye_Framework.FrameworkStatus.NOT_SUPPORT) return;

        if (SRanipal_Eye_Framework.Instance.EnableEyeDataCallback && !eye_callback_registered)
        {
            SRanipal_Eye_v2.WrapperRegisterEyeDataCallback(
                Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic) EyeCallback));

            eye_callback_registered = true;
        }
        else if (!SRanipal_Eye_Framework.Instance.EnableEyeDataCallback && eye_callback_registered)
        {
            SRanipal_Eye_v2.WrapperUnRegisterEyeDataCallback(
                Marshal.GetFunctionPointerForDelegate((SRanipal_Eye_v2.CallbackBasic) EyeCallback));

            eye_callback_registered = false;
        }

        var openness = GetEyeOpenness(EyeIndex.LEFT);
        Debug.Log(openness);
    }

    private float GetEyeOpenness(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetEyeOpenness(eyeIndex, out var openness, _eyeData);
        return openness;
    }

    private static void EyeCallback(ref EyeData_v2 eye_data)
    {
        _eyeData = eye_data;
    }
}


計測結果は次のようになります。

Update の処理が 0.250ms とかなり小さくなりました。取得処理自体はコールバックになったので、Analyzer で取得している箇所が見えなくなりました。

f:id:vxd-naoshi-19961205-maro:20220222005221p:plain



比較結果

SRanipal_Eye_AIP.GetEyeData_v2 の Analyzer を見て頂けたらわかると思いますが、アイトラッキングの情報を取得する処理だけで、PlayerLoop の6割以上を占めています。

対して、コールバックを使用した方は PlayerLoop 自体が0.250msとかなり小さくなりました。 もし、アイトラッキングの情報を取得する場合であれば、少しめんどくさいですが、コールバックで情報を取得したほうが良いでしょう。



ひとりごと

アイトラッキングではコールバックによる取得ができるのに、フェイストラッキングではコールバックがないのが悔やまれる。(アイトラッキングに比べたら小さいが 1ms ぐらい取得にかかる)



最後に

【Vive Pro Eye】アイトラッキングで取得した瞳の位置をモデルに反映させる

f:id:vxd-naoshi-19961205-maro:20220219203040g:plain

始めに

半年前ぐらいにVive Pro Eyeを触りましてちょっとしか遊んでなかったので、今回は取得した情報をモデルに反映させてみました。

今回使用するモデルはこちらです。

booth.pm

Vive Pro Eyeについては過去記事を参考にして下さい。SDKについて簡単にまとめています。

過去記事リンク



Vive Pro Eyeから瞳の座標を取得する

Vive Pro Eyeから瞳の座標を取得するためには SRanipal_Eye_v2.GetPupilPosition を使います。 position には瞳の座標が -1 ~ 1 の範囲で代入されます。(xは右、yは上方向が1に値する)

using UnityEngine;
using ViveSR.anipal.Eye;

public class GetPupilPositionSample : MonoBehaviour
{
    void Update()
    {
        var leftPupilPosition = GetPupilPosition(EyeIndex.LEFT);
        var rightPupilPosition = GetPupilPosition(EyeIndex.RIGHT);

        Debug.Log($"Left Pupil Position:{leftPupilPosition}");
        Debug.Log($"right Pupil Position:{rightPupilPosition}");
    }
    
    private Vector2 GetPupilPosition(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetPupilPosition(eyeIndex, out var position);
        return position;
    }
}


また、SRanipal_Eye_v2 を使用するにはUnityのHierarchyに SRanipal_Eye_Framework コンポーネントを追加して、Enable Eye VersionVersion 2 に設定する必要があります。

f:id:vxd-naoshi-19961205-maro:20220219141731p:plain



モデルの瞳を動かす

モデルの瞳の構造とその問題

今回使用するモデルの瞳は眼球のように球体ではなく、瞳の絵が白目に張ってある状態でした。

f:id:vxd-naoshi-19961205-maro:20220219143107p:plain


なので、瞳のオブジェクトを回転させても瞳の絵が回転するだけになりました。

f:id:vxd-naoshi-19961205-maro:20220219143452g:plain


また、瞳のオブジェクトを平行移動させた場合は瞳の絵が目からはみ出てしまう問題がありました。

f:id:vxd-naoshi-19961205-maro:20220219143917p:plain



任意の中心点を元に瞳を回転させる

考えてみれば当たり前ですが、現実の人の瞳は眼球の中心点を元に回転しています。この考えに基づいて、モデルの瞳のオブジェクトも同様に中心点を決めて回転させます。

以下モデルの瞳のオブジェクト回転させるサンプルです。

MaplePupilRotateCheck

using UnityEngine;

public class MaplePupilRotateCheck : MonoBehaviour
{
    [SerializeField] private Transform _leftEyePupil;
    [SerializeField] private Transform _rightEyePupil;

    [SerializeField] private Transform _leftEyeCenter;
    [SerializeField] private Transform _rightEyeCenter;

    [SerializeField] private float _angleX;
    [SerializeField] private float _angleY;

    private Vector3 _originLeftEyePupilPosition;
    private Quaternion _originLeftEyePupilRotation;
    private Vector3 _originRightEyePupilPosition;
    private Quaternion _originRightEyePupilRotation;

    private Vector3 _leftDistance;
    private Vector3 _rightDistance;
    
    void Start()
    {
        _originLeftEyePupilPosition = _leftEyePupil.localPosition;
        _originLeftEyePupilRotation = _leftEyePupil.localRotation;
        _originRightEyePupilPosition = _rightEyePupil.localPosition;
        _originRightEyePupilRotation = _rightEyePupil.localRotation;
        
        Reset();
    }

    void Update()
    {
        // 実行中に中心点を移動させたとき用のリセット処理
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Reset();
            return;
        }

        var localRotation = Quaternion.AngleAxis(_angleX, Vector3.right) * Quaternion.AngleAxis(_angleY, Vector3.up);
        
        _leftEyePupil.localRotation = localRotation;
        _rightEyePupil.localRotation = localRotation;

        _leftEyePupil.localPosition = localRotation * _leftDistance + _leftEyeCenter.localPosition;
        _rightEyePupil.localPosition = localRotation * _rightDistance + _rightEyeCenter.localPosition;
    }

    private void Reset()
    {
        _angleX = 0;
        _angleY = 0;
        _leftEyePupil.localPosition = _originLeftEyePupilPosition;
        _leftEyePupil.localRotation = _originLeftEyePupilRotation;
        _rightEyePupil.localPosition = _originRightEyePupilPosition;
        _rightEyePupil.localRotation = _originRightEyePupilRotation;
        _leftDistance = _leftEyePupil.localPosition - _leftEyeCenter.localPosition;
        _rightDistance = _rightEyePupil.localPosition - _rightEyeCenter.localPosition;
    }
}


上記のサンプルの実行結果は次のようになります。上手く中心点を設定することで、瞳がはみ出ることなく動かせています。

f:id:vxd-naoshi-19961205-maro:20220219155143g:plain


解説

初期化時に中心点から瞳までのベクトルを求めます。

この値は回転させるときに使用します。

        _leftDistance = _leftEyePupil.localPosition - _leftEyeCenter.localPosition;
        _rightDistance = _rightEyePupil.localPosition - _rightEyeCenter.localPosition;


Update で指定されたx軸とy軸の回転から回転量 localRotation を求めて瞳をローカル空間で回転させます。

        var localRotation = Quaternion.AngleAxis(_angleX, Vector3.right) * Quaternion.AngleAxis(_angleY, Vector3.up);
        
        _leftEyePupil.localRotation = localRotation;
        _rightEyePupil.localRotation = localRotation;


次に瞳の座標を設定します。初期化時に求めた中心点から瞳までのベクトルに先程求めた回転量を掛け合わせて、中心点の座標に加算します。

こうすることで、中心点を元に瞳を回転させられます。

        _leftEyePupil.localPosition = localRotation * _leftDistance + _leftEyeCenter.localPosition;
        _rightEyePupil.localPosition = localRotation * _rightDistance + _rightEyeCenter.localPosition;



※ Transform.RotateAroundを使わなかった理由

任意のオブジェクトを中心に回転させるメソッドとして Transform.RotateAround があります。

Transform-RotateAround - Unity スクリプトリファレンス

こちらは回転量を指定して現在の座標から回転させるメソッドになります。(上記のプログラムでは元の座標から回転させる

Vive Pro Eyeで取得した瞳の座標から Transform.RotateAround を使用するには前フレームとの誤差を取得して回転量を求める等かなり面倒くさい方法をしなければならなかったため、使用しませんでした。



Vive Pro Eyeと組み合わせる

後はVive Pro Eyeと瞳を動かすプログラムを組み合わせます。

以下サンプルです。

MaplePupilRotate

using UnityEngine;

public class MaplePupilRotate : MonoBehaviour
{
    [SerializeField] private Transform _leftPupil;
    [SerializeField] private Transform _rightPupil;
    
    [SerializeField] private Transform _leftEyeCenter;
    [SerializeField] private Transform _rightEyeCenter;

    [SerializeField] private float _maxAngleX;
    [SerializeField] private float _maxAngleY;

    private Vector3 _leftEyeCenterLocalPosition;
    private Vector3 _rightEyeCenterLocalPosition;

    private Vector3 _distanceFromLeftCenterToPupil;
    private Vector3 _distanceFromRightCenterToPupil;

    public void Awake()
    {
        _leftEyeCenterLocalPosition = _leftEyeCenter.localPosition;
        _rightEyeCenterLocalPosition = _rightEyeCenter.localPosition;

        _distanceFromLeftCenterToPupil = _leftPupil.localPosition - _leftEyeCenterLocalPosition;
        _distanceFromRightCenterToPupil = _rightPupil.localPosition - _rightEyeCenterLocalPosition;
    }

    public void SetLeftPupilPosition(Vector2 pupilPosition)
    {
        var localRotation = CalculateEyeRotation(pupilPosition);
        _leftPupil.localRotation = localRotation;
        _leftPupil.localPosition = localRotation * _distanceFromLeftCenterToPupil + _leftEyeCenterLocalPosition;
    }

    public void SetRightPupilPosition(Vector2 pupilPosition)
    {
        var localRotation = CalculateEyeRotation(pupilPosition);
        _rightPupil.localRotation = localRotation;
        _rightPupil.localPosition = localRotation * _distanceFromRightCenterToPupil + _rightEyeCenterLocalPosition;
    }

    private Quaternion CalculateEyeRotation(Vector2 pupilPosition)
    {
        // MEMO: pupilPositionは瞳の座標、計算では瞳の回転角度なので x, y が逆になっている
        var angleX = pupilPosition.y * _maxAngleX;
        var angleY = pupilPosition.x * _maxAngleY;

        return Quaternion.AngleAxis(angleX, Vector3.left) * Quaternion.AngleAxis(angleY, Vector3.up);
    }
}

MaplePupilSample

using UnityEngine;
using ViveSR.anipal.Eye;

public class MaplePupilSample : MonoBehaviour
{
    [SerializeField] private MaplePupilRotate _maplePupilRotate;
    
    void Update()
    {
        var leftPupilPosition = GetPupilPosition(EyeIndex.LEFT);
        var rightPupilPosition = GetPupilPosition(EyeIndex.RIGHT);
        
        _maplePupilRotate.SetLeftPupilPosition(leftPupilPosition);
        _maplePupilRotate.SetRightPupilPosition(rightPupilPosition);
    }
    
    private Vector2 GetPupilPosition(EyeIndex eyeIndex)
    {
        SRanipal_Eye_v2.GetPupilPosition(eyeIndex, out var position);
        return position;
    }
}



実行結果

上記のサンプル以外にまばたきするプログラムも動かしています。

f:id:vxd-naoshi-19961205-maro:20220219203040g:plain


また、シェイプキーでしいたけ目にしたり目を小さくしても問題なく動きました。

f:id:vxd-naoshi-19961205-maro:20220219203659g:plain


最後にウィンクをしてみました。(苦手なので許してください)瞳がぶれたりしないか心配でしたが、問題なさそうです。

f:id:vxd-naoshi-19961205-maro:20220219204214g:plain



最後に

アバターに瞬きや目線を加えるだけで、かなり生きてる感が増して楽しかったです。

今後はフェイシャルトラッカーにも挑戦してみる予定です。

また、丁度VRCでもOSCを使えばVive Pro Eyeやフェイシャルトラッカーの情報も送信できるみたいなので機会があれば触ってみようと思います。

【雑談】2021年振り返り

始めに

今月何もブログを書けてなく、特に開発系で書きたい内容も無いので雑に今年一年を振り返ろうと思います。

ものすごく雑に書くので、誤字脱字やおかしな文章があると思いますがご了承ください。



2021年で変わったこと

あまり大きな変化があったわけではないですが、変わったことをまとめます。

社会人になった

3月に大学院を卒業して、4月から社会人として働き始めました。 仕事内容はおおよそ皆さんが想像している通りだと思います。

仕事はずっとリモートだったので、基本的にずっと引きこもりをしていました。 仕事し始めは慣れるか心配でしたが、今ではある程度慣れてきたのかなと思います。

来年から出社に切り替わるそうなので、早起き頑張ります。



関東に引っ越した

大学院を卒業した後に福島の会津から関東圏に引っ越しました。

元々都会が苦手でかつ人混みが大嫌いなのですが、住んでいるところはそこまで人が多いわけではないので、とても快適に過ごせています。

ただし、買い物などで都会に行ったときは流石に今でも慣れません。ものすごくイライラしてしまいます。

また、関東圏では雪が全く降らないので寂しい思いをしています。 またいつか雪国に住みたいと夢見ています。



ランニング始めた

3年ほど筋トレを続けてまして、その延長としてランニングを始めました。

もともと私は足は速くなく、かつ体力もないので走るのは大嫌いでしたが、いざランニングを始めてみると意外と楽しかったです。

ただし、ランニングを始めたとしても体重の変化はあまりなく、プラマイゼロでした。ダイエットをするには食事制限が一番のようです。



今年やったこと

VR機材を買った

ちょっと前から興味があった Vive Pro Eye や 今年発売された Vive Facial Tracker 、欲しかった Valve Index Controller を買いました。

shitakami.hatenablog.com

shitakami.hatenablog.com

shitakami.hatenablog.com


また、追加で1つ Vive Tracker を購入しました。来年ももう少しVR機材を買おうと思います。



マトリックスのエフェクトを作った

マトリックスの新作映画のPVが発表されたタイミングで作成しました。物心ついたときからずっと好きだった作品だったため、興奮が抑えきれませんでした。

shitakami.hatenablog.com

shitakami.hatenablog.com

shitakami.hatenablog.com


子供時代に夢見ていたものを今自分で作れるのは感慨深いと感じました。また、このときが自分の中で一番盛り上がった瞬間だったと思います。



できなかったこと

できなかったことを振り返るのはあまり気分的によろしくないと思いますが、来年には達成するために少しまとめます。

個人開発

個人開発はずっとやっていましたが、取っ掛かり部分で悩み答えが出ずに頓挫するを何度も繰り返しました。

原因として挙げられるのは大きく2つと考えています。

  • 知ってはいたが理解できていない知識
  • 始めから完璧を目指していた

仕事などで得た知識を個人開発にも活かそうとやってみましたが、やはり中途半端な知識では上手く形に出来ませんでした。まずは小さい段階を踏みつつ、理解を固めていくのが良いと反省しました。

次に最初から完成形を作ろうとし過ぎて、作りたいものをまとめるのが困難になっていました。今は反省して、最小機能から1機能ずつ追加してコードを整理する作業を繰り返して上手くコントロールしています。



関東圏観光

ずっと家に引きこもっていたので、行ってみたいところを観光してません。これはお金と時間と相談しつつ来年から少しずつやっていこうと思います。



芸術系分野をやってみる

いつかはやってみたいなって言って、ずっとやってません。

基本的に、自分が今やっていることから関連することをやってみたりするのですが(ランニングがその例)芸術系にはなかなか手が出ていません。

やらなかった原因として、今やりたいことから取捨選択をしたときに捨てられたことが挙げられます。なので、まぁ大きく後悔はしてないけどやっぱりいつかは手を出したいと思ってます。



2021年ハマったもの

OK GO

今年の始めにこのグループのMVを見てハマりました。 彼らのMVはどれも衝撃的なものばかりなので、興味があれば見てほしいです。


www.youtube.com

いつか、こんなことをVRでやってみたい。



ENDER LILIES

ja.enderlilies.com

Steamで買ってプレイしました。このゲームを知った理由がサウンドを好きな音楽グループのMiliが担当されたことでした。

ゲーム自体も非常によく、個人的な印象として死にゲーでありながらも、システム面が非常に親切なので苦になることがなかったです。

また、音楽も最高でサウンドトラックを購入しました。


www.youtube.com



筋トレ

社会人になったら筋トレやめてしまうかと思ってましたが、学生のころより励むようになりました。

新しいダンベルを買うことはなく、持っている器具で引き続き筋トレをやってましたが、少しずつこなせる回数が増えました。

結果として、去年より少し筋肉が付いたかと感じてます。

ただし、ほとんどずっと筋トレばかりやってるので、少し時間を削ってほかに充てたいと考えてます。



来年について

取り合えず今行っている個人開発を終わらせることを目標に考えています。

これが終わらないとやりたいことができないので、これを第一に進める予定です。

次にハーフマラソンに出る予定です。出る理由としてはランニングを始めたことと、親父もでるので競ういい機会かと思いました。

親父の方は恐らく競争とは思ってないと思いますが、10年以上もランニングをしている人なのでこちらも相当の準備をしなければとなってます。

あとは来年は一年通して健康でいられることを努めようと思います。

ComputeShaderとDrawMeshInstancedIndirectでGraphicsBufferを使う

始めに

去年にこの記事を拝見し、GraphicsBufferなるものの存在を知りました。

zenn.dev



また、この記事内にて遠い将来ComputeBufferが無くなり、GraphicsBufferに切り替わる予定とのことでした。 (GraphicsBuffer, Mesh vertices and Compute shaders - Unity Forum より)

なので、一度簡単にGraphicsBufferについて触ってみようと思います。



GraphicsBuffer

簡単にGraphicsBufferについてまとめます。

コンストラク

次のように定義されています。

public GraphicsBuffer (GraphicsBuffer.Target target, int count, int stride);


第一引数のtargetではGraphicsBufferの用途を指定します。

例えば、Graphics.DrawMeshInstancedIndirectのargumentsBufferとして使用する場合はGraphicsBuffer.Target.IndirectArguments、ComputeShaderのStructuredBufferとして使用する場合はGraphicsBuffer.Target.Structuredを指定します。


また、このGraphicsBuffer.Targetは次のようbitで定義されています。 (Unity公式リポジトリより

なので、OR演算などで組み合わせられるようですが、このあたりは詳しくは分かりません。

        public enum Target
        {
            Vertex            = 1 << 0,
            Index             = 1 << 1,
            CopySource        = 1 << 2,
            CopyDestination   = 1 << 3,
            Structured        = 1 << 4,
            Raw               = 1 << 5,
            Append            = 1 << 6,
            Counter           = 1 << 7,
            IndirectArguments = 1 << 8,
            Constant          = 1 << 9,
        }


第二引数、第三引数についてはComputeBufferと同じく、データの長さとデータサイズを表しています。



ComputeBufferからGraphicsBufferに書き換える

既存のComputeBufferはGraphicesBufferに置き換えられます。

StructuredBuffer, AppendStructuredBuffer, ConsumeStructuredBuffer

GraphicsBufferはComputeShaderで使われるStructuredBufferとして扱うことができます。

ComputeBufferとの違いとしては、ComputeBufferではComputeBufferType.Structuredを指定する必要はありませんでしたが、GraphicsBufferでは明示的に指定しなければいけません。

// ComputeBuffer
m_particleBuffer = new ComputeBuffer(m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_particleBuffer.SetData(particles);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_particleBuffer );

// GraphicsBuffer
m_graphicsBuffer_Particle = new GraphicsBuffer(GraphicsBuffer.Target.Structured, m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_graphicsBuffer_Particle.SetData(particles);
m_particleCalclator.SetBuffer(m_calcParticlePositionKernel, "_Particle", m_graphicsBuffer_Particle);



また、AppendStructuredBuffer, ConsumeStructuredBufferの場合はGraphicsBuffer.Target.Appendを指定することで扱えます。

その他、AppendStructuredBufferで使われるComputeBuffer.CopyCountGraphicsBuffer.CopyCountに置き換えられます。

m_particlePoolBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Append, m_instanceCount, Marshal.SizeOf(typeof(int)));
m_particlePoolBuffer.SetCounterValue(0);

GraphicsBuffer.CopyCount (m_particlePoolBuffer, m_particlePoolCountBuffer, 0);



Graphic.DrawMeshInstancedIndirect

インスタンシングで使われるDrawMeshInstancedIndirectでもGraphicsBufferを使えます。

この場合はGraphicsBuffer.Target.IndirectArgumentsを指定します。

// ComputeBuffer
m_argsBuffer = new ComputeBuffer(1, args.Length * sizeof(uint), ComputeBufferType.IndirectArguments);
m_argsBuffer.SetData(args);

// GraphicsBuffer
m_graphicsBuffer_Args = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, args.Length * sizeof(uint));
m_graphicsBuffer_Args.SetData(args);

Graphics.DrawMeshInstancedIndirect(
    m_mesh,
    0,
    m_instanceMaterial,
    m_bounds,                                                                 
    m_graphicsBuffer_Args,
    0,
    null,
    m_shadowCastingMode,
    m_receiveShadows
);


このほかにGraphics.DrawProcedural系のメソッドでも使えるそうです。



Material.SetBuffer

Graphic.DrawMeshInstancedIndirectを使う場合はMaterial.SetBufferでデータを渡していましたが、こちらもGraphicsBufferを使えます。

m_graphicsBuffer_Particle = new GraphicsBuffer(GraphicsBuffer.Target.Structured, m_instanceCount, Marshal.SizeOf(typeof(Particle)));
m_graphicsBuffer_Particle.SetData(particles);
m_instanceMaterial.SetBuffer("_ParticleBuffer", m_graphicsBuffer_Particle);


総括

おおよそ触ってみて、これまでの私が書いてきたプログラムはすべてGraphicsBufferに置き換えられることがわかりました。



GraphicsBufferの利点

ComputeBufferからGraphicsBufferに置き換えられることは分かりましたが、それらを置き換えることで処理が高速になるかと言われればそうではないようです。

しかし、GraphicsBufferではMeshの頂点情報やインデックス情報を扱えるようになったようです。

私自身Meshの頂点情報やインデックス情報を扱った機会はほとんどありませんが、Mesh.GetVertexBufferで直接`GraphicsBufferに頂点情報を受け取れるようになります。 (私の環境 Unity2020.3.7f1ではまだ用意されていなかった)

そうなると、過去記事で頂点情報から力場を求めるプログラムで無駄なメモリを使わずに頂点情報を扱うことができます。

shitakami.hatenablog.com


// 前はm_verticesに一度移さないといけない手間があった
m_skinnedMeshRenderer.BakeMesh(m_bakeMesh);
m_bakeMesh.GetVertices(m_vertices);
m_positionsBuffer.SetData(m_vertices);

// 頂点情報をそのままGraphicsBufferに渡せる
m_skinnedMeshRenderer.BakeMesh(m_bakeMesh);
m_positionBuffer = m_bakeMesh.GetVertexBuffer();


また、Graphics.DrawProcedural系でも頂点情報などをGraphicsBufferでそのまま渡せるらしいので、この辺りは少し調べてみたいです。



まとめ

簡単に調べてみて、ComputeBufferの使い方とほぼ同じなので抵抗なく使えることがわかりました。

最初はGraphicsBufferに変わることで得られる恩恵とは無縁かと思っておりましたが、過去のプログラムの面倒くさい部分が解消されることが分かったので良かったです。

最近はGPU系のことを全くやっていないので、また何かやってみようと思います。



参考

始めにも載せましたが、きっかけはこちらの記事でした。また、リンクの内容もとても良かったので詳しく知りたい方は参考にしてください。

zenn.dev



また、UnityStationでもGraphicsBufferについて詳しくお話しされていたのでお時間があるときに見てみると良いと思います。

learning.unity3d.jp

Riderのブレークポイント機能メモ

始めに

つい最近からUnityでのコーディングでRiderを使い始めました。

このRiderのブレークポイントを使ったデバッグ機能について簡単にメモを残そうと思います。

また、何かしら新しく知った機能があれば追加していく予定です。


ブレークポイントを設定する

Riderでブレークポイントを設定する方法が3つほどあります。

  • マウスで一時停止したい行のガター領域をクリック
  • 一時停止したい行にカーソルをつけて F9
  • ツールバーのRun -> Toggle Breakpoint -> Line Breakpoint

私は主に1番目の方法でブレークポイントを設定しています。

f:id:vxd-naoshi-19961205-maro:20211024161103g:plain


実際にUnity実行中にブレークポイントで一時停止したい場合は、Riderの右上の虫のボタンを押す or Alt + F5デバッグを開始する必要があります。

f:id:vxd-naoshi-19961205-maro:20211024161521p:plain



ブレークポイントで設定できる項目

ブレークポイントの赤い ● を右クリックすることで、一時停止する条件やログの出力など細かい設定が可能です。

また、Moreをクリックすることでブレークポイントウィンドウが開きます。

f:id:vxd-naoshi-19961205-maro:20211024162043p:plain


有効 / 無効の設定

Enabledのチェックボックスブレークポイントが有効か無効かを設定できます。無効にした場合は、そのブレークポイントで一時停止しません。

無効になったブレークポイントのマークは中が塗りつぶされていない〇になります。

f:id:vxd-naoshi-19961205-maro:20211024162846g:plain



一時停止する条件の設定

Conditionで条件式を書くことで、一時停止する条件を設定できます。

例えば、プログラムが多くのGameObjectにアタッチされている場合はその全てのGameObjectでブレークポイントを設定した行が実行されて、非常に煩わしいことになります。

そのような場合はGameObjectの名前を指定することで、調べたいオブジェクトのみで一時停止させられます。

また、Shift + Enter で複数の条件式を記述できます。

f:id:vxd-naoshi-19961205-maro:20211024163801p:plain



Conditionのほかに、Hit Count(実行された回数)で一時停止する設定もできます。

基本的な比較に加えて Multiple of で指定した値の倍数回ブレークポイントを実行した際に一時停止できます。

f:id:vxd-naoshi-19961205-maro:20211024165613p:plain



状態の評価、出力

Logの項目で、Debug Outputにブレークポイントの到達通知、変数の値の評価と出力、スタックトレースなどを出力できます。

  • "Breakpoint hit" message : ブレークポイントに到達したことを出力する
  • Stack trace : 呼び出し元のツリーを出力する
  • Evaluate and log : 出力する文字列を設定できる。変数の値の出力や条件式の記述も可能。

f:id:vxd-naoshi-19961205-maro:20211024172534p:plain



上の設定でブレークポイントに到達した場合は、次のようにDebug Outputに出力されます。

f:id:vxd-naoshi-19961205-maro:20211024172752p:plain


もし、ブレークポイントで一時停止せずにLogの出力のみがしたい場合は Suspend execution のチェックを外すと可能です。

f:id:vxd-naoshi-19961205-maro:20211024173351p:plain



Unityの一時停止ポイントに変換

「Convert to Unity pausepoint」をクリックすることでブログラムの一時停止からUnityの一時停止に変換できます。

このとき、Logの機能のみ使えなくなります。

f:id:vxd-naoshi-19961205-maro:20211024173728p:plain



プログラム一時停止中に出来ること

ブレークポイントに到達したときにもStack traceを確認したり、変数の値を見たり設定したりできます。

変数の値を見る

ブレークポイントに到達したときにプリミティブ型の値はそのまま表示されます。

f:id:vxd-naoshi-19961205-maro:20211024183735p:plain


プリミティブ型だけでなく、配列やリスト、クラスなどのデータも詳しく見ることができます。

f:id:vxd-naoshi-19961205-maro:20211024184036g:plain


また、DebbugerにあるVariable TabでUnityのSceneの状態や変数の値などを確認できます。

f:id:vxd-naoshi-19961205-maro:20211024185130p:plain



任意の処理を実行する(変数の値の変更、メソッドの実行など)

DebuggerにあるImmediate Windowで任意の処理を実行できます。

この機能を使うことで変数の値を変えたり、メソッドを呼び出したりできます。

f:id:vxd-naoshi-19961205-maro:20211024185404g:plain



呼び出し元を調べる

ブレークポイントに至るまでにどのように関数が呼ばれているかを調べられます。

また、クリックすることでコードジャンプできるので呼び出し元の詳細も分かります。

f:id:vxd-naoshi-19961205-maro:20211024185927p:plain



最後に

私が使っている、知っている機能について簡単にまとめました。

また新しく機能について学んだら追加していこうと考えています。



参考

公式のドキュメントの方が詳しくまとめられているので、その他の機能について知りたい方は参考にして下さい。

ブレークポイント | JetBrains Rider

中断されたプログラムを調べる | JetBrains Rider

デバッグウィンドウ | JetBrains Rider