夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

EyeTrackingDemo-05-Visualizer  ~MixedRealityToolkitExamplesを触ってみる。 その④ DrawOnTexture

本日はMRTKExamples勉強枠です。

EyeTrackingDemo-05-Visualizerシーンを覗いていきます。

〇EyeTrackingDemo-05-Visualizer

HoloLens 2ではユーザーの目線を取得するEyeTrackingが使用可能になりました。

これによって開発者はユーザーの目を使った操作を提供するだけではなくどこを見ていたかの情報を得ることができるようになりました。

EyeTrackingDemo-05-Visualizerシーンではユーザーがどこを見ていたかをヒートマップで見ることができる例が紹介されています。

f:id:Holomoto-Sumire:20210125185639j:plain

本日はこのヒートマップを作成するコンポーネントである[DrawOnTexture]コンポーネントを見ていきます。

〇Draw OnTextureとは?

ユーザーがどこを見ていたかを可視化するヒートマップを作成するためのコンポーネントです。

youtu.be

全コードはMRTKのGithubにあります。

github.com

〇処理の流れ

ここからは処理の順にコードを追っていきます。

●Start関数

 private void Start()
        {
            if (EyeTarget != null)
            {
                EyeTarget.WhileLookingAtTarget.AddListener(OnLookAt);
            }
        }

Start関数ではEyeTargetが存在する場合[WhileLookintAtTarget]に[OnLookAt]のイベントを渡します。

EyeTrackingは[Draw OnTexture]がアタッチされている自分自身のオブジェクトから[EyeTrackingTarget]コンポーネントを取得しています。

private EyeTrackingTarget EyeTarget
        {
            get
            {
                if (eyeTarget == null)
                {
                    eyeTarget = this.GetComponent<EyeTrackingTarget>();
                }
                return eyeTarget;
            }
        }

[EyeTrackingTarget]コンポーネントのWhileLookingAtTargetはユーザーがターゲットを見続けたときに発火するイベントです。

microsoft.github.io

つまりユーザーがターゲットを見続けた場合[OnLookAt]のイベントが実行されます。

●OnLookAt

OnLookAtでは次の3つの条件がそろった場合DrawAtThisHitPosが実行されます。

・UseLiveInputStream が真

・EyeTargetが存在する

・EyeTargetのIsLookedAtイベントが新=オブジェクトを見つめている

 public void OnLookAt()
        {
            if (UseLiveInputStream && (EyeTarget != null) && (EyeTarget.IsLookedAt))
            {
                DrawAtThisHitPos(EyeTrackingTarget.LookedAtPoint);
            }
        }

DrawAtThisHitPosメソッドの引数にはEyeTrackingTarget.LookedAtPointが渡されます。

これはユーザーが見つめている座標になります。

●DrawAtThisHitPos

 public void DrawAtThisHitPos(Vector3 hitPosition)
        {
            Vector2? hitPosUV = GetCursorPosInTexture(hitPosition);
            if (hitPosUV != null)
            {
                StartCoroutine(DrawAt(hitPosUV.Value));
            }
        }

hitPosUVが存在する場合コルーチンによってDrawAt(hitPosUV.Value)が実行されます。

hitPosUVはNull許容型のVector2型で次のようになります。

  private Vector2? GetCursorPosInTexture(Vector3 hitPosition)
        {
            Vector2? hitPointUV = null;

            try
            {
                Vector3 center = gameObject.transform.position;
                Vector3 halfsize = gameObject.transform.localScale / 2;

                // Let's transform back to the origin: Translate & Rotate
                Vector3 transfHitPnt = hitPosition - center;

                // Rotate around the y axis
                transfHitPnt = Quaternion.AngleAxis(-(this.gameObject.transform.rotation.eulerAngles.y - 180), Vector3.up) * transfHitPnt;

                // Rotate around the x axis
                transfHitPnt = Quaternion.AngleAxis(this.gameObject.transform.rotation.eulerAngles.x, Vector3.right) * transfHitPnt;

                // Normalize the transformed hit point to as UV coordinates are in [0,1].
                float uvx = (Mathf.Clamp(transfHitPnt.x, -halfsize.x, halfsize.x) + halfsize.x) / (2 * halfsize.x);
                float uvy = (Mathf.Clamp(transfHitPnt.y, -halfsize.y, halfsize.y) + halfsize.y) / (2 * halfsize.y);
                hitPointUV = new Vector2(uvx, uvy);
            }
            catch (UnityEngine.Assertions.AssertionException)
            {
                Debug.LogError(">> AssertionException");
            }

            return hitPointUV;
        }

center=自身のオブジェクトの座標

halfsize=自身のオブジェクトのscaleの半分

※Null許容型

docs.microsoft.com

●DrawAt
        private IEnumerator DrawAt(Vector2 posUV)
        {
            if (MyDrawTexture != null)
            {
                // Reset on first draw
                if (neverDrawnOn)
                {
                    for (int ix = 0; ix < MyDrawTexture.width; ix++)
                    {
                        for (int iy = 0; iy < MyDrawTexture.height; iy++)
                        {
                            MyDrawTexture.SetPixel((int)(ix), (int)(iy), new Color(0, 0, 0, 0));
                        }
                    }
                    neverDrawnOn = false;
                }

                // Assign colors
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, true, true));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, true, false));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, false, true));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, false, false));
                yield return null;

                MyDrawTexture.Apply();
            }
        }

DrawAtでは最初の処理で塗りつぶしのリセットを行っています。

 // Reset on first draw
                if (neverDrawnOn)
                {
                    for (int ix = 0; ix < MyDrawTexture.width; ix++)
                    {
                        for (int iy = 0; iy < MyDrawTexture.height; iy++)
                        {
                            MyDrawTexture.SetPixel((int)(ix), (int)(iy), new Color(0, 0, 0, 0));
                        }
                    }
                    neverDrawnOn = false;
                }

これはテクスチャのピクセルのRGBAを0にしています。

                // Assign colors
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, true, true));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, true, false));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, false, true));
                yield return null;

                StartCoroutine(ComputeHeatmapAt(posUV, false, false));
                yield return null;

                MyDrawTexture.Apply();
            }
        }

その後フレームおきに[ComputeHeatmapAt()]の処理が行われます。

●ComputeHeatmapAt
 private IEnumerator ComputeHeatmapAt(Vector2 currPosUV, bool positiveX, bool positiveY)
        {
            yield return null;

            // Determine the center of our to be drawn 'blob'
            Vector2 center = new Vector2(currPosUV.x * MyDrawTexture.width, currPosUV.y * MyDrawTexture.height);
            int sign_x = (positiveX) ? 1 : -1;
            int sign_y = (positiveY) ? 1 : -1;
            int start_x = (positiveX) ? 0 : 1;
            int start_y = (positiveY) ? 0 : 1;

            for (int dx = start_x; dx < MyDrawTexture.width; dx++)
            {
                float tx = currPosUV.x * MyDrawTexture.width + dx * sign_x;
                if ((tx < 0) || (tx >= MyDrawTexture.width))
                    break;

                for (int dy = start_y; dy < MyDrawTexture.height; dy++)
                {
                    float ty = currPosUV.y * MyDrawTexture.height + dy * sign_y;
                    if ((ty < 0) || (ty >= MyDrawTexture.height))
                        break;

                    Color? newColor = null;
                    if (ComputeHeatmapColorAt(new Vector2(tx, ty), center, out newColor))
                    {
                        if (newColor.HasValue)
                        {
                            MyDrawTexture.SetPixel((int)(tx), (int)(ty), newColor.Value);
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

ここではピクセル単位で指定されたテクスチャを塗りつぶす処理を行っています。

この部分がヒートマップの肝になっているようです。

〇その他

[ComputeHeatmapAt]、[DrawAt]という名前の関数が多数ありますが使われていない(テスト?開発時の名残?)コードが多く残っていますが、加工すれば非常に使いやすい新しい表現ができる可能性があります。