HoloLens上で壁に動画を正しい向きで表示する方法

#環境
Microsoft HoloLens
Unity 2017.1
Visual Studio 2017

# いきさつ
Unityの勉強を始めて2週間たった頃、HoloLensで空間認識をして壁に動画を貼りたいなーって思いました。

Mixed Reality ToolkitのSpatial Mapping機能を使えば、便利なことに自動で壁を認識してオブジェクトを作ってくれます。
このときUnity上では"SurfacePlane"というMixed Reality toolkit内に存在するプレハブのコピーが作られますが、これはUnityのCubeオブジェクトをただ変形しただけのものです(たぶん)。

つまり、HoloLens上で壁に動画を貼るということはUnityおなじみのCubeオブジェクトに動画を貼るのと同じタスクなわけです。

Unity5.6からVideoPlayerという機能が追加されたそうなので、それを使ってCubeオブジェクトに動画を貼ってみました。
こちらの記事などを参考にするとよいと思います。
https://qiita.com/gshirato/items/1500f61b408e7af69453

すると、一部の面では動画が回転されて表示されてしまいました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/176122/4955c1d8-8a1b-b149-bc2d-b86bf519aac5.png)

これはおそらく、動画の向きが頂点のインデックスの呼び出される順番に依存して決まるからだと思います。
実際、平面を表すPlaneオブジェクトやQuadオブジェクトでは正しい向きで表示してくれます。

※この記事でいう動画の「正しい向き」とはオブジェクトのZ軸周りの回転角が0°のときに動画が逆さに表示されていない状態だと定義します。

# 解決方法
Cubeの頂点インデックスの順番を変えればうまくいくと思いますが、そもそも壁をCubeで表したくない(2面だけで十分なのに6面も描画されてしまう)ので壁のオブジェクトをCubeからQuadに変換するコードを書きました。

流れはこんな感じです。
1. Cubeのワールド座標変換前のローカル座標系において、変換後に壁として使われる(つまり最も面積の大きい)2面を6面から探索する。
2. ローカル座標系においてその2面と同じ座標になるようにQuadオブジェクトを生成して動かす。
3. Cubeのマテリアルやワールド変換行列などをQuadに適用する。

急いで書いたので汚いコードになりましたが、とりあえずこれで上手くいきます。

あとはZ軸周りの回転角を0度にすれば正しい向きで表示されます。そのコードはここでは省略します。rotation.z = 0fにしてlocalScaleをいじるだけです。


```c#

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

public class RemakePlane : MonoBehaviour
{
    // Cubeの頂点座標
    Vector3[] cubeLocalSharedVert = new Vector3[24]; //ローカル座標系での頂点座標(ダブりあり)
    List<Vector3> cubeLocalVert = new List<Vector3>(); //ローカル座標系での頂点座標(ダブりなし) 
    Vector3[] cubeWorldVert = new Vector3[8]; //ワールド座標系での頂点座標

    // Quadオブジェクトを2つ生成して子オブジェクトとして登録する関数
    public GameObject Cube2Quad(GameObject _parent)
    {
        MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();
        cubeLocalSharedVert = meshFilter.mesh.vertices;

　　　　 // ダブってる頂点を削除する
        cubeLocalVert.Add(cubeLocalSharedVert[0]);
        for(int i = 0; i < cubeLocalSharedVert.Length; ++i)
        {
            for (int j = 0; j < cubeLocalVert.Count; ++j)
            {
                if (cubeLocalSharedVert[i] == cubeLocalVert[j])
                {
                    break;
                }
                if (j == cubeLocalVert.Count - 1)
                {
                    cubeLocalVert.Add(cubeLocalSharedVert[i]);
                }
            }
        }
        
        // 面のインデックスを取得
        int[,] surfaces = new int[6, 4];
        int[] index = new int[6];

        for (int i = 0; i < index.Length; ++i)
        {
            index[i] = 0;
        }

        // 面の取得
        for (int i = 0; i < cubeLocalVert.Count; ++i)
        {
            if (cubeLocalVert[i].x > 0)
            {
                surfaces[0, index[0]] = i;
                index[0]++;
            }
            else if (cubeLocalVert[i].x < 0)
            {
                surfaces[1, index[1]] = i;
                index[1]++;
            }

            if (cubeLocalVert[i].y > 0)
            {
                surfaces[2, index[2]] = i;
                index[2]++;
            }
            else if (cubeLocalVert[i].y < 0)
            {
                surfaces[3, index[3]] = i;
                index[3]++;
            }

            if (cubeLocalVert[i].z > 0)
            {
                surfaces[4, index[4]] = i;
                index[4]++;
            }
            else if (cubeLocalVert[i].z < 0)
            {
                surfaces[5, index[5]] = i;
                index[5]++;
            }
        }

        // 変換行列Get
        Matrix4x4 matrix = meshFilter.transform.localToWorldMatrix;

        for (int i = 0; i < cubeLocalVert.Count; ++i)
        {
            cubeWorldVert[i] = matrix.MultiplyPoint(cubeLocalVert[i]);
        }

        // 最も面積の大きい面を探す
        List<float> areas = new List<float>();
        List<float> areas_dash = new List<float>();

        for (int i = 0; i < 6; ++i)
        {
            Vector3[] verticesInSurface = new Vector3[4];
            for (int j = 0; j < verticesInSurface.Length; ++j)
            {
                verticesInSurface[j] = cubeWorldVert[surfaces[i, j]];
            }

            List<float> lengths = new List<float>();
            for (int j = 0; j < 3; ++j)
            {
                lengths.Add(Vector3.Distance(verticesInSurface[0], verticesInSurface[j + 1]));
            }
            lengths.Sort();
            areas.Add(lengths[0] * lengths[1]);
            areas_dash.Add(lengths[0] * lengths[1]);
        }

        areas_dash.Sort();
        areas_dash.Reverse();

        // 最も面積の大きい二つの面を探す
        int[] indicesOfMaxAreaSurfaces = new int[2];

        if (Mathf.Approximately(areas_dash[0], areas_dash[1]))
        {
            int k = 0;
            for (int i = 0; i < areas.Count; ++i)
            {
                if (Mathf.Approximately(areas_dash[0], areas[i]))
                {
                    if(k == 2)
                    {
                        Debug.Log("Error");
                        break;
                    }
                    indicesOfMaxAreaSurfaces[k] = i;
                    k++;
                }
            }
        }
        else
        {
            for (int i = 0; i < areas.Count; ++i)
            {
                if (Mathf.Approximately(areas_dash[0], areas[i]))
                {
                    indicesOfMaxAreaSurfaces[0] = i;
                }
                if (Mathf.Approximately(areas_dash[1], areas[i]))
                {
                    indicesOfMaxAreaSurfaces[1] = i;
                }
            }
        }

        // ローカル座標系でQuadを新しく作成し、ワールド座標変換を行う
        for (int i = 0; i < indicesOfMaxAreaSurfaces.Length; ++i)
        {
           Vector3[] newQuadPos = new Vector3[4];

            Vector3[] cubePos = new Vector3[4];
            for(int j = 0; j < 4; ++j)
            {
                cubePos[j] = cubeLocalVert[surfaces[indicesOfMaxAreaSurfaces[i], j]];
            }


            // どの軸で貼られた面なのかを調べる
            float verticalX = 0f, verticalY = 0f, verticalZ = 0f;
            for(int j = 0; j < 4; ++j)
            {
                verticalX += cubePos[j].x;
                verticalY += cubePos[j].y;
                verticalZ += cubePos[j].z;
            }

            if (!Mathf.Approximately(verticalX, 0f))
            {
                float sign = Mathf.Sign(cubePos[0].x);

                // x = 0.5 の面の場合
                if (Mathf.Approximately(sign, 1f))
                {
                    newQuadPos[0] = new Vector3(0.5f, -0.5f, -0.5f);
                    newQuadPos[1] = new Vector3(0.5f, 0.5f, 0.5f);
                    newQuadPos[2] = new Vector3(0.5f, -0.5f, 0.5f);
                    newQuadPos[3] = new Vector3(0.5f, 0.5f, -0.5f);
                }
                // x = -0.5 の面の場合
                else
                {
                    newQuadPos[0] = new Vector3(-0.5f, -0.5f, 0.5f);
                    newQuadPos[1] = new Vector3(-0.5f, 0.5f, -0.5f);
                    newQuadPos[2] = new Vector3(-0.5f, -0.5f, -0.5f);
                    newQuadPos[3] = new Vector3(-0.5f, 0.5f, 0.5f);
                }
            }
            else if(!Mathf.Approximately(verticalY, 0f))
            {
                float sign = Mathf.Sign(cubePos[0].y);

                // y = 0.5 の面の場合
                if (Mathf.Approximately(sign, 1f))
                {
                    newQuadPos[0] = new Vector3(-0.5f, 0.5f, -0.5f);
                    newQuadPos[1] = new Vector3(0.5f, 0.5f, 0.5f);
                    newQuadPos[2] = new Vector3(0.5f, 0.5f, -0.5f);
                    newQuadPos[3] = new Vector3(-0.5f, 0.5f, 0.5f);
                }
                // y = -0.5 の面の場合
                else
                {
                    newQuadPos[0] = new Vector3(-0.5f, -0.5f, 0.5f);
                    newQuadPos[1] = new Vector3(0.5f, -0.5f, -0.5f);
                    newQuadPos[2] = new Vector3(0.5f, -0.5f, 0.5f);
                    newQuadPos[3] = new Vector3(-0.5f, -0.5f, -0.5f);
                }
            }
            else if(!Mathf.Approximately(verticalZ, 0f))
            {
                float sign = Mathf.Sign(cubePos[0].z);
                
                // z = 0.5 の面の場合
                if (Mathf.Approximately(sign, 1f))
                {
                    newQuadPos[0] = new Vector3(0.5f, -0.5f, 0.5f);
                    newQuadPos[1] = new Vector3(-0.5f, 0.5f, 0.5f);
                    newQuadPos[2] = new Vector3(-0.5f, -0.5f, 0.5f);
                    newQuadPos[3] = new Vector3(0.5f, 0.5f, 0.5f);
                }
                // z = -0.5 の面の場合
                else
                {
                    newQuadPos[0] = new Vector3(-0.5f, -0.5f, -0.5f);
                    newQuadPos[1] = new Vector3(0.5f, 0.5f, -0.5f);
                    newQuadPos[2] = new Vector3(0.5f, -0.5f, -0.5f);
                    newQuadPos[3] = new Vector3(-0.5f, 0.5f, -0.5f);
                }
            }
            else
            {
                print("Error");
            }

            // ゲームオブジェクトを新規作成
            GameObject quad = GameObject.CreatePrimitive(PrimitiveType.Quad);
            quad.transform.parent = _parent.transform;

            // Cubeの面をQuadとして定義しなおす
            Mesh mesh = quad.GetComponent<MeshFilter>().mesh;
            mesh.vertices = newQuadPos;
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();

            // 座標変換
            Transform quadTrans = quad.GetComponent<Transform>();
            quadTrans.position = matrix.MultiplyPoint(quadTrans.position);
            quadTrans.rotation = transform.rotation;
            quadTrans.localScale = transform.localScale;

            // マテリアルの引継ぎ
            quad.GetComponent<Renderer>().material = gameObject.GetComponent<Renderer>().material;

        }
        return _parent;
    }
}
```