したかみ ぶろぐ

Unity成分多め

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

始めに

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

 

shitakami.hateblo.jp

 

 

そのときに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.hateblo.jp

shitakami.hateblo.jp

shitakami.hateblo.jp

 

 

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



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

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

 

shitakami.hateblo.jp

shitakami.hateblo.jp

shitakami.hateblo.jp

 

 

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



できなかったこと

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

個人開発

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

原因として挙げられるのは大きく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

簡単にシェーダーが出来るiOSアプリ「Shade」の紹介

始めに

最近少しUnityのシェーダーで映画Matrixのエフェクトを作成しました。

過去記事

そこから、少しシェーダー熱が再発して手軽にシェーダーで遊びたいなーとなったのでiOSアプリ「Shade」を使って見ました。

その感想を簡単に紹介していきます。

iOSアプリ「Shade」について

簡単にシェーダーが遊べるアプリ「Shade」があります。

こちらは無料で14日間体験でき、それ以降は有料となります。

Shade

Shade

  • Two Lives Left
  • グラフィック/デザイン
  • 無料
apps.apple.com


このアプリではUnityのShader graphのようにノードベースのプログラミングでシェーダーを作成することが可能です。

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



基本的な機能

簡単に触って分かった基本的な機能について説明します。

Surfaceノード

ShadeではこのSurfaceノードに値を設定して色や法線情報を設定したり、頂点座標を変化できます。

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


また、これらの値は固定値として設定することも可能です。

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


そのほかにも、オブジェクトの生成個数やShading Model (physical, unlit, custom) やRender Queue, Cull Faceも設定できます。

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



ノード

Shadeでは数多くのノードが用意されており、それらを使ってシェーダーを作成できます。

ここでノードすべての解説をするには数が多すぎるので、カテゴリごとに簡単にまとめます。

カテゴリ 解説
Properties UnityシェーダーのPropertiesに相当。数値やテクスチャ、色などを設定できます。
Input GPUから入力される値に相当。UVや頂点座標、実行からの経過時間などが受け取れます。
Maths 計算系のノード。四則演算や比較演算、frac関数、floor関数などがあります。
Geometric こちらも計算系のノードではありますが、Mathsよりも高度な計算が用意されています。UVの変換やベクトルの内積外積が行えます。
Trigonometry 三角関数系のノード。ラジアンと度数法の変換やsin、asin関数があります。
Noise 2~4次元のPerlin NoiseやVoronoi図、乱数があります。
Generatice Height MapからNormal Mapへの変換やBlur、Tilingなどの変換、Textureのバッファが出来ます。
Organisation ノードをまとめたり、変数に保存する機能があります。
Color 色のBlendやRGBとHSBの変換ができます。
Lighting SurfaceノードでShading Modelをcustomに設定したときに、Lightingの設定ができます。
Raymarching Raymarchingの基本的な計算のノードが用意されています。(Raymarchingを知らないので、何ができるかあまりわかっていない)


個々のノードには解説がついているので取っつきやすいです。

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



Preview設定

この設定でScene上の設定が可能です。

オブジェクトにレンダリングするか、平面にレンダリングするかなど設定でき、さらにARで現実にレンダリングされたオブジェクトを置いたり、自分の顔にシェーダーを設定できます。

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


そのほかにScene上の光源の設定やBloom設定ができます。

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

Preview Model

Scene上のモデルの設定ができます。

座標、回転、スケーリングに加えてモデルの変更が行えます。

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



Shadeのサンプルについて

このアプリでは豊富なサンプルが用意されています。

ノードの使い方がわからなかったり、このアプリでどこまで出来るかの参考になります。

ここではサンプルのgifを2つほど載せます。

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

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



作ってみたもの

2週間ほど試してできたものをここで紹介します。

Triplanar

少し前にTriplanarのシェーダーをUnityで勉強したので、Shadeでも作成してみました。 Unityで平面マッピング・Triplanarをする - なおしのこれまで、これから

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


中心から放射するアニメーション

遊んでいるときにたまたまtwitterappleの広告を見たので、簡単に真似してみたものです。

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

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


Cubeを整列してアニメーション

Surfaceノードでオブジェクトの複製、ノードでそれぞれの座標を計算して作成してみました。

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



使用感について

あまりノードコーディングをしたことがなかったので、始めは慣れませんでしたが、使っているうちにちょっとずつ考えたことプログラムに落とし込めるようになりました。

コーディングの際は、ショートカットキーで簡単にノードの検索、作成ができるのでサクサクプログラミングが出来て楽しいです。

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



また、ノードが増えたとしてもOrganisationのノードを使うことで意外とスッキリまとめられるので、コーディングが辛くなることはありませんでした。

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

最後に

簡単にShadeについての紹介をしました。

使って見て感じたこととして、このようになりました。

  • 意外となんでもできる
  • iPadで手軽に遊べるため、取っつきやすい
  • 自分の知識に比べてオーバースペック

自分のシェーダーに関する知識はあまりないので、このアプリで遊びつつ少しずつシェーダーについて勉強していこうと思います。



参考

3dnchu.com

iOSアプリ「ショートカット」で卒業した推しの去年のツイートを表示する


始めに

少し前に私が推していたVTuberが晴れて卒業されました。

たまに、その方のアーカイブを見て楽しんだり少し寂しい気持ちになったりしています。

そんなとき、ふと推しが活動していた時代に戻りたいと思ってしまいました。


作ったショートカット

このURLから作成したショートカットが取得できると思います。

https://www.icloud.com/shortcuts/d8e68650ef304573aa1ba90f3a46748e


twitterの検索機能について

ご存じの方もいらっしゃると思いますが、twitterには便利な検索機能があります。 ここではこの記事内で使うコマンドを載せます。

コマンド 機能
from:(ユーザID) 特定のアカウントからのツイートを検索
to:(ユーザID) 特定のアカウントへのリプライやメンションを検索
since:yyyy-MM-dd yyyy年MM月dd日以降のツイートを検索
until:yyyy-MM-dd yyyy年MM月dd日以前のツイートを検索


これら以外にも多くのコマンドがあるので、興味がある方はこちらを参照してください。

yonoi.com



去年の推しのツイートを検索する

twitterの検索で次のように検索することで去年のツイートを取得できます。

from:推しのtwitterID since:2020-10-16 until:2020-10-17

これを自動的に行うショートカットを作成すれば、簡単に去年の推しのツイートを取得できます。



やること概要

iOSに入っているアプリ「ショートカット」で次のことを行わせます。

これにより自動的に去年の推しのツイートを検索してくれます。

  1. 現在の日付を取得

  2. 現在の日付から1年減らす

  3. 過去の日付という変数に保存

  4. さらに1日減らす 

  5. 過去の日付 - 1という変数に保存

  6. 過去の日付 - 1から過去の日付までのあいだのツイートを検索するurlを作成

  7. urlを開く



ショートカット作成

1. 現在の日付を取得

始めに現在の日付を設定します。 このノード?が見つからない場合は、「日付」で検索するとすぐに見つかると思います。

f:id:vxd-naoshi-19961205-maro:20211017160025j:plain



2. 現在の日付から1年減らす

twitterの検索の「until」に相当する値をここで求めます。

今回の目的は「1年前の日付までのツイートを取得すること」なので、現在の日付から1年引きます。

もし、表示したい期間が別であればここで1日前など設定できます。

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



3. 過去の日付という変数に保存

先ほど求めた日付は検索のときに使用するので、変数に保存します。

ここでは分かりやすくするために過去の日付と名付けました。

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



また、日付のフォーマットをtwitterの検索で使える形に直します。

twitterの検索では「yyyy-MM-dd_hh:mm:ss」というフォーマットになります。

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



4. さらに1日減らす 

次にtwitterの検索の「since」に相当する日付を計算します。

今回表示したい期間は1年前の1日前~1年前の1日間にツイートされたものを表示したいので、さらに1日減らします。

もし、より多くの期間のツイートを表示したい場合は3日や7日減らすと良いでしょう。

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



5. 過去の日付 - 1という変数に保存

こちらもまた後で使うので、変数に保存します。

こちらも忘れずに「yyyy-MM-dd_hh:mm:ss」でフォーマットします。

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



6. 過去の日付 - 1から過去の日付までのあいだのツイートを検索するURLを作成

次に、過去の日付 - 1から過去の日付のあいだの推しのツイートを取得するURLを作ります。

始めに、テキストに推しのtwitterのユーザIDを記載します。 ここでは、実験的に自分のtwitterID「CCPJ_a」を書いています。

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



次に検索のURLを作成します。

URLを調べるために、Chromeアプリでtwitterを開いて、「from:推しのユーザID since:2020-10-16 until:2020-10-17」を検索します。

次にそのときのURLをコピーして、テキストにペーストします。

f:id:vxd-naoshi-19961205-maro:20211017162435j:plain



次にそのURL内で推しのユーザIDや取得する期間などがあるので、それらを保存した変数などに置き換えます。

これによって、「去年の推しのツイート」を検索するURLができました。

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



7. urlを開く

最後に、「URLを開く」を追加して検索URLを開くようにします。

あとは右上の再生ボタンで実行すると勝手にtwitterアプリが起動して、検索結果を表示してくれます。

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



ホーム画面に追加する

あとはショートカットをホーム画面に追加することで、推しのツイートをすぐに見られるようになります。

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



最後に

iOSアプリの「ショートカット」が意外にも強力だと感じました。

文字列を結合したり、テキストの中に変数をそのまま追加できたりととても便利でした。

少し不満があるとすれば、変数が使いにくいと感じました。

また何か思いついたら、別のショートカットを作るかもしれません。



ちょっとした雑談

ここからはこのショートカットを作るまでの裏話。

もともとはTwitter APIを使って、推しの過去ツイートを取得するアプリを作ろうと考えておりました。

しかし、調べてみると無料版のTwitter APIでは5日前ぐらいのツイートしか取得できないとわかりました。また、有料版では過去のツイートを取得できるが料金が高く、そう何度も取得できないとなりました。

結果、Twitter APIを使ったアプリ開発は出来ないという結論でした。

じゃあスクレイピングTwitterのサイトを解析するかという話も出たのですが、Twitterではスクレイピングは禁止されていました。

なので、最終的な結論は去年の推しのツイートを自動的に検索してくれるものを作ろうとなりまして、その方法を調べました。

始めはWindowsアプリになるかと思っていましたが、iOSの「ショートカット」でもしかしたら出来るかとなって、模索した結果で作ることができました。

つまり、色々あったという話でした。