鳩の溜まり場

猫か鳩になりたい

画像から簡単な3Dモデルを生成してみた【Unity】

はじめに

レベルは低く単なる自己満レベル+備忘録的なものです。

やること

紆余曲折を経て今現在,視差画像を3Dモデルに変換するということをしています。 簡単に説明すると視差画像のグレースケールを取得して,Polygonに反映することで立体的に見えるということです。

環境

手順

※ここに書いてあるソースコードは特殊な部分があるので一度流し読みしてからもう一度見た方が多少わかりやすいです。

まずは3Dモデルにするための視差画像をTexture2Dとして読み込んで,Read/Write Enabledにチェックマークをつけます。

このとき,画像の解像度がちっちゃい場合(大きいときもかも?),表示した際にアンチエイリアスがガンガンに効いてしまうので,効かないように設定しましょう。方法についてはggってください。(Unity ぼやけるあたりで出るはずです)

これでソースコードから画像を弄れるようになったので,グニグニしていきます。 とはいってもまずは下地が必要なのでPolygonを作ります。Polygonについての説明はたいして理解できてないので他の方の記事を参考にしてください。ここでは原理というかどうすると動くかについて説明します。

とりあえずPolygon生成のソースコードをドカン。変数名の英語力には触れないでください。(英語頑張りたい)

    private Mesh meshData;

    private int scale; // 解像度=Polygonの頂点数だと多すぎるからサイズを小さくするための比率
    private int specialWidth; // scaleを適用した横
    private int specialHeight; // scaleを適用した縦

    /*
    @summary
    Pictureのデータから3Dに変換するクラス
    
    @args
    width:画像の横幅(Scale適用前)
    height:画像の縦幅(Scale適用前)
    scale:解像度どのくらい下げるか(引数=2だったら2ドットで1ドットと認識) 
    */
    public MakeStage(int width, int height, int scale)
    {
        // Polygonの頂点の数を1/(scale)にしてサイズはそのまま
        specialWidth = width / scale;
        specialHeight = height / scale;

        int vertexNumber = specialWidth * specialHeight; // Polygonの頂点数
        Vector3[] normals = new Vector3[vertexNumber]; // Polygonの法線
        Vector3[] polygonsVertex = new Vector3[vertexNumber]; // Polygonの頂点
        Vector2[] uvForTexture = new Vector2[vertexNumber]; // UVをつかって貼るテクスチャ用

        int surfaceNumber = vertexNumber * 2; // Polygonの面数(三角形で1面だから2倍)
        int[,] bindOrderTwoArray = new int[surfaceNumber, 3]; // Polygonの頂点を結ぶ順番

        /*
         ----------- ①頂点の設定 -----------
        */
        // Polygonの頂点を設定,UV座標の設定(テクスチャの貼り付け)
        int counter = 0;
        for (int y = 0; y < specialHeight; y++)
        {
            for (int x = 0; x < specialWidth; x++)
            {
                // (scale)×(scale)のpixelで1つ分のPolygon
                Vector3 polygonVertexPosition = new Vector3((scale * x) + (scale / 2), (scale * y) + (scale / 2), 0);
                polygonsVertex[counter] = polygonVertexPosition;

                // UV座標の設定 分母の最大が1だから,xをspecialWidthで,yをspecialHeightで割る
                uvForTexture[counter] = new Vector2((float)x / (specialWidth - 1), (float)y / (specialHeight - 1));

                counter++;
            }
        }
        // ----------- ① end -----------


        /*
         ----------- ②頂点を結ぶ順番の設定 -----------
        */
        // Polygonの頂点を結ぶ順番を設定
        int polygonVertexNow = 0;
        counter = 0;
        for (int y = 0; y < specialHeight - 1; y++)
        {
            for (int x = 0; x < specialWidth; x++)
            {
                if (x == specialWidth - 1)
                {
                    polygonVertexNow++;
                    break;
                }

                /*
                |---------|
                | 左上の /|
                | 三角 /  |
                |   / 右下|
                | / の三角|
                |_________|
                 */
                // 左上側の三角形のPolygon
                bindOrderTwoArray[counter, 0] = polygonVertexNow;
                bindOrderTwoArray[counter, 1] = polygonVertexNow + specialWidth;
                bindOrderTwoArray[counter, 2] = polygonVertexNow + specialWidth + 1;

                counter++;

                // 右下側の三角形のPolygon
                bindOrderTwoArray[counter, 0] = polygonVertexNow;
                bindOrderTwoArray[counter, 1] = polygonVertexNow + specialWidth + 1;
                bindOrderTwoArray[counter, 2] = polygonVertexNow + 1;

                counter++;

                polygonVertexNow++;
            }
        }

        // bindOrderTwoArray -> bindOrderに変換(二次元 -> 一次元)
        int forBindOrder = surfaceNumber * 3;
        int[] bindOrder = new int[forBindOrder];
        counter = 0;
        for (int c = 0; c < forBindOrder; c += 3)
        {
            bindOrder[c] = bindOrderTwoArray[counter, 0];
            bindOrder[c + 1] = bindOrderTwoArray[counter, 1];
            bindOrder[c + 2] = bindOrderTwoArray[counter, 2];

            counter++;
        }
        // ----------- ② end -----------


        /*
         ----------- ③法線の設定 -----------
        */
        // Polygonの法線を設定
        for (counter = 0; counter < vertexNumber; counter++)
        {
            normals[counter] = new Vector3(0, 0, -1);
        }
        // ----------- ③ end -----------

        meshData = new Mesh();

        meshData.vertices = polygonsVertex;
        meshData.triangles = bindOrder;
        meshData.normals = normals;
        meshData.uv = uvForTexture;

        meshData.RecalculateBounds();
    }

全部説明するとQiitaのサーバーの容量に申し訳ないので(),重要なとこだけ書いておきます。あとは色々いじってみてください。

まず,PolygonにはMeshを使うので母体となるMeshを作っておきます。 ここに,Polygonをペタペタしていくんですが,このコードを見ると大変そうに見えますよね? しかし実はたった3つの要素を設定すれば出てきます。

        meshData.vertices = polygonsVertex;
        meshData.triangles = bindOrder;
        meshData.normals = normals;

下側にあるこの3つです。

① 頂点の設定

まずMesh.verticesですが,これはPolygonの頂点を作ります。ここで作った頂点を結ぶことでPolygonが表示される仕組みです。

左下の原点からx座標1段埋める→y座標1段上げるの順で頂点を打ってます。ここに関しては②で使いやすいように各自変えてみてください。

② 頂点を結ぶ順番の設定

次に頂点を結ぶ順番をMesh.trianglesで設定します。 これが若干詰まりポイントで,何故か二次元配列を使わないんです。 まずPolygonは3つの頂点を結んで三角形の面を繋ぎ合わせることで形を成します。その時オモテ面とウラ面が存在するんですが,Polygonはオモテからのみしか描画されません。ゲームとかやってて裏世界に入ると大半の地面が透明で,ところどころ地面が見えるのはこの原理です。(たぶん) UnityではこのPolygonの頂点を時計回りに結んだ面がオモテ面になります。

しかし,先に述べた通り,これを一次元配列で表現します。そのため,以上の例だと,[0,3,1,0,2,3]となります。 これがわかりにくいなと感じた+拡張性(?)を考えて私は一度二次元配列にこんな[0][0,3,1] [1][0,2,3]感じで入れてから最終的に一次元配列に変換しました。

③ 法線の設定

最後にMesh.normalsです。 法線は面の向きを決めます。自分もあやふやな部分も多いので他のサイト様をご参照ください。

④ Let's 描画。

Graphics.DrawMesh(Meshデータ, メッシュの位置(原点), メッシュの回転, マテリアル, 0);

詳しくはreferenceみてね。 https://docs.unity3d.com/ja/2018.1/ScriptReference/Graphics.DrawMesh.html

これまで理解できればPolygonは作れます。できなかった方は適宜調整してください。

⑤ 立体感を出そう

    // モノクロ画像のGrayscale取得
    public float[] GetGrayscale()
    {
        float[] grayscale = new float[width * height];
        Color[] pixels = pictureData.GetPixels();

        int counter = 0;
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                grayscale[counter] = pixels[counter].grayscale;
                counter++;
            }
        }

        return grayscale;
    }

これで各ピクセルのGrayscaleを取得します。pictureDataには最初に設定した画像をTexture2Dとして読み込んで入れます。

    // GrayScale等の情報でPolygonの起伏を変更したりする
    public void SetPolygonStatus(float[] grayscale, int width)
    {
        Vector3[] polygonsVertex = meshData.vertices; // Polygonの頂点

        int counter = 0;
        int grayCounter = 0;
        for (int y = 0; y < specialHeight; y++)
        {
            for (int x = 0; x < specialWidth; x++)
            {
                int irregularity = -1 * (scale * 20); // 負の値にすると白が手前に出てくる
                polygonsVertex[counter].z = irregularity * grayscale[grayCounter];
                counter++;
                grayCounter += scale;
            }
            grayCounter += (scale - 1) * width;
        }

        meshData.vertices = polygonsVertex;

        meshData.RecalculateBounds();
    }

ここで取得したGrayscaleの値をZ座標にそれぞれ正しく反映させることで立体感のあるPolygonの出来上がりです。

番外編 ソースコードのscaleって何?

最初からソースコードに書いてあるscaleやらspecialWidthやら理解を苦しめるものが多いかと思いますが,これは1ピクセルに1つのPolygonの頂点を打たないようにするためのものです。Unityの仕様上なのかこちらの環境の問題かよくわかりませんが,一定数以上Polygonの面ができないため,1ピクセルに頂点1つではなく,2×2ピクセルに頂点1つ等にするソースコードになってます。要は解像度を下げてる感覚です。 ここはscaleに対して何ピクセルで頂点1つにするか値を指定すればそれっぽく出ます。

結果

上のソースコードを少しずついじるとこのようながPolygonが作れるようになります。

結果.png

白い部分が手前に出て黒い部分が凹んでいるかと思います。 一応この元画像置いておきます。

デバック用画像.png

詰まった点

割とPolygonの作成に関しては調べると出てきたりするのですぐできましたが,解像度を落としてPolygonを作ると結構めんどくさいので,チャチャっとやりたい方は上のソースコード参考にしてみてください(参考に値するかは別として)。

感想

ゲーム系統のハッカソン参加したいのでなにかあったら教えていただけると嬉しいです。参加したいです。
Twitter: mokapants