質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.12%

Unity:レンダリング結果の射影変換について

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 2,684

Kapustin

score 1005

前提・実現したいこと

Unityのポストエフェクトとして、レンダリングした結果に射影変換を適用させようとしています。
メインカメラにアタッチしたコンポーネントのOnRenderImageにて下記のシェーダを適用させています。

発生している問題・エラーメッセージ

上記の処理を行っても、変換結果がOpenCvのそれと異なってしまい、悩んでおります。
皆さまのお力をお借りしたく、どうぞよろしくお願いいたします。

該当のソースコード

Shader "Hidden/MappingShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _translate ("Translate", Float) = 0
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            uniform float4x4 _mv;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                float4 vec = v.vertex;

                float x = vec.x;
                float y = vec.y;
                float z = vec.z;
                float w = vec.w;

                float px = x * _mv[0][0] + y * _mv[0][1] + z * _mv[0][2] + w * _mv[0][3];
                float py = x * _mv[1][0] + y * _mv[1][1] + z * _mv[1][2] + w * _mv[1][3];
                float pz = x * _mv[2][0] + y * _mv[2][1] + z * _mv[2][2] + w * _mv[2][3];
                float pw = x * _mv[3][0] + y * _mv[3][1] + z * _mv[3][2] + w * _mv[3][3];

                   vec.x = px;
                   vec.y = py;
                   vec.z = pz;
                   vec.w = pw;


                v2f o;
//                o.vertex = mul(_mv, v.vertex);
                o.vertex = vec;
                o.vertex = UnityObjectToClipPos(o.vertex);

                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
    private Material m_Material;
    private Matrix4x4 matrix;

    void Awake()
    {
        Shader shader = Shader.Find("Hidden/MappingShader");
        m_Material = new Material(shader);
    }

    void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        Graphics.Blit(source, destination, m_Material);
    }

    void Update()
    {
        Material.SetMatrix("_mv", matrix);
    }

試したこと

行列の転置を試したり計算式を一から書いてみましたが、もっと別なところに原因があるのではないかと考えています。
他に思い当たる原因などありましたら、ご教授いただければ幸いです。

補足情報(言語/FW/ツール等のバージョンなど)

Unity 5.6.1
macOS Sierra

# 行列のサンプルです。この変換行列での実行結果の画像を添付します。
0.74035    0.16225    0.00000    0.00069
0.13807    0.89541    0.00000    0.00028
0.00000    0.00000    1.00000    0.00000
149.00000    105.00000    0.00000    1.00000


イメージ説明

追記

OpenCv版の座標変換は、下記のような処理を行っています。
この行列を4x4に変換したものが、最初のサンプルの行列になっています。
正確に言うと(すこしややこしいですが)openframeworks の ofxQuadWarpというアドオンを利用し、getMatrix()にて得られる変換行列(4x4)を利用しています。
こちらは内部的には(精度の問題で)cvFindHomographyを利用しているようですが、転置行列になるだけで結果はほぼ変わりません。

    // openframework上で動かしています。

    ofImage image;
    image.load("in.jpg");

    cv::Mat cvInImage = cv::Mat(image.getHeight(), image.getWidth(), CV_8UC3, image.getPixels().getData());
    cv::Mat cvOutImage = cvInImage.clone();

    vector<cv::Point2f> src;
    vector<cv::Point2f> dst;

    for(int i = 0; i < 4; i ++){
        // 変換前の座標配列:入力画像の左上、右上、右下、左下の座標
        src.push_back(cv::Point2f(warper.srcPoints[i].x, warper.srcPoints[i].y));
        // 変換後の座標配列:最初の画像の左(OpenCvの表記)の白い四角の四隅にある黄色い点の座標(左上、右上、右下、左下の順)
        dst.push_back(cv::Point2f(warper.dstPoints[i].x, warper.dstPoints[i].y));
    }

    cv::Mat warp_matrix = cv::getPerspectiveTransform(src, dst);
    cv::warpPerspective(cvInImage, cvOutImage, warp_matrix, cvInImage.size());


    ofImage outImage;
    outImage.allocate(image.getHeight(), image.getWidth(), image.getImageType());
    outImage.setFromPixels(cvOutImage.ptr(), cvOutImage.cols, cvOutImage.rows, image.getImageType(), false);
    outImage.save("out.jpg");

    cout << warp_matrix << endl;
// OpenCv版の変換行列の出力結果
[0.7403473533105349, 0.1380676946623043, 148.9999999999996;
  0.1622461658588558, 0.895410834059531, 104.9999999999999;
  0.0006888663351174518, 0.0002752964241302562, 1]

追記2

warperに設定している変換前座標と変換後座標です。これで得られる行列は先述と同値です。

// SourceRectの設定値:画面のフルスクリーンサイズを設定しています。
// setSourceRect(ofRectangle(0, 0, ofGetScreenWidth(), ofGetScreenHeight()));

srcPoint[0] : 0, 0, 0
srcPoint[1] : 1440, 0, 0
srcPoint[2] : 1440, 900, 0
srcPoint[3] : 0, 900, 0

dstPoint[0] : 149, 105, 0
dstPoint[1] : 610, 170, 0
dstPoint[2] : 598, 511, 0
dstPoint[3] : 219, 730, 0
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Bongo

    2017/05/23 20:08 編集

    すみませんが、二点お伺いします。Unity側のカメラはOrthographicになっているでしょうか。これがPerspectiveだと、UnityObjectToClipPosが変なことをするかもしれません(もしPerspectiveならUnityObjectToClipPosの行を削除してみて、描画がどうなるかお試しください)。もう一つは、描画しようとしているMeshのverticesの内容はどうなっていますでしょうか。これがwarperのsrcPointsに与えた四隅の座標と異なると、うまくいかないかも...と思われました。

    キャンセル

  • Bongo

    2017/05/24 06:24 編集

    後で考えたら、この質問は少々的外れだった気がしてきました。質問を変えさせてください。 warperにsetSourceRectやsetSourcePointsで変換前座標を設定されているかと思いますが、どのような値に設定されていますでしょうか?
    また、参考としてwarperが変換したdstPointsの示す四隅の座標も見てみたいのですが、よろしいでしょうか。

    キャンセル

  • Kapustin

    2017/05/24 17:18

    度々ご質問いただきありがとうございます。各座標の値を追記いたしました。 なお、念のため上記の方法も試してみましたが、やはり思う結果にはなりませんでした。 考察いただき本当にありがとうございます。引き続きよろしくお願いいたします。

    キャンセル

回答 1

checkベストアンサー

+2

ここまで情報ご提供いただいたからには何かしらの成果を出したいところです...

ちょっと調べたところ、シェーダーに入力される頂点座標は画像の左下隅原点、右端がX1.0、上端がY1.0のようです。一方、warperの行列は画像左上隅原点、右端が画像幅、下端が画像高さとなる座標系を前提としているわけですから、この差を埋めるため変換を追加してみました。

また、変換後のW成分を残さないとテクスチャがひずむため、UnityObjectToClipPosを削除して、代わりにX・Y座標のみ2倍に拡大し、Wの分だけX・Y座標をずらしてo.vertexに渡しています(ちなみに、o.vertex = float4((vec.xyz / vec.w) * 2.0 - 1.0, 1.0);のようにW成分を潰すとテクスチャがひずむと思います)。

Shader "Hidden/MappingShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _translate("Translate", Float) = 0
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            uniform float4x4 _mv;

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                float4 vec = mul(_mv, v.vertex); // ここで変換されたベクトルのW成分(奥行きに比例)はテクスチャを正しく貼るために残したい
                v2f o;
                o.vertex = float4(vec.xy * 2.0 - vec.w, vec.zw); // UnityObjectToClipPos(vec)だとW成分が消されるようなので、自前で2倍に拡大し、消失点が画像隅から画面中心に変わるので、それを加味してX、YからWを引く
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
private Material m_Material;
private float imageWidth; // インスタンス変数追加
private float imageHeight; // インスタンス変数追加
private Matrix4x4 matrix;
private Matrix4x4 unityCoordToWarperCoordMatrix; // インスタンス変数追加
private Matrix4x4 warperCoordToUnityCoordMatrix; // インスタンス変数追加

void Awake()
{
    Shader shader = Shader.Find("Hidden/MappingShader");
    m_Material = new Material(shader);

    // warperで変換行列を求める際に使った画像サイズが必要、実際には外部から与える?
    imageWidth = 1440.0f;
    imageHeight = 900.0f;

    // X0.0~1.0を0.0~1440.0に、Y0.0~1.0を0.0~-900.0になるよう反転・引き延ばした上で、Yに900.0足してY900.0~0.0にする
    unityCoordToWarperCoordMatrix = Matrix4x4.TRS(new Vector3(0.0f, imageHeight, 0.0f), Quaternion.identity, new Vector3(imageWidth, -imageHeight, 1.0f));

    // warper座標をUnity座標に戻すには逆行列で変換すればよいはず...
    warperCoordToUnityCoordMatrix = unityCoordToWarperCoordMatrix.inverse;
}

void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    Graphics.Blit(source, destination, m_Material);
}

void Update()
{
    // warperが生成した行列、実際には外部から与える?
    matrix = new Matrix4x4();
    matrix.SetColumn(0, new Vector4(0.74035f, 0.16225f, 0.00000f, 0.00069f));
    matrix.SetColumn(1, new Vector4(0.13807f, 0.89541f, 0.00000f, 0.00028f));
    matrix.SetColumn(2, new Vector4(0.00000f, 0.00000f, 1.00000f, 0.00000f));
    matrix.SetColumn(3, new Vector4(149.00000f, 105.00000f, 0.00000f, 1.00000f));

    // warper行列の前後に追加の変換を入れてシェーダーに与える
    Matrix4x4 compositeMatrix = warperCoordToUnityCoordMatrix * matrix * unityCoordToWarperCoordMatrix;

    // SetMatrixはインスタンスメソッドなので、Materialをm_Materialに変更
    m_Material.SetMatrix("_mv", compositeMatrix);
}

これならどうでしょうか?
もう一つの案として、warperで行列を作る時点でUnity座標系を使ってしまうというのも可能かもしれません。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/05/25 09:57

    ありがとうございます!上記の方法で希望する結果になりました!幸せです。
    実際には、シェーダーに渡す行列を逆行列にする必要がありましたが、他のパラメータでも試してみたところ、期待する動作になりました。
    ご教授いただいたシェーダの計算式や変換行列の加工部分については理解するまでもう少し時間がかかりそうですが、、頑張ってみます。お力添え頂き、本当にありがとうございました!

    キャンセル

  • 2017/05/25 10:07

    いえいえ、こちらもUnityシェーダーの勉強になりたいへん有意義でした。
    おそらく、ご提示のUnity側の結果が、いかにもアフィン変換しただけのような平行四辺形になってしまったのも、UnityObjectToClipPosがW座標を無視してしまうことが原因のように思われます。

    キャンセル

  • 2017/05/25 11:30

    UnityObjectToClipPosではfloat3が引数でWを1.0に固定してしまっているのですね。それでこのような結果になってしまうというのは大変勉強になりました。異なる座標系で処理しようとしていた事も、それに気づけなかった要因だったように思います。鋭いご指摘頂きありがとうございます。
    また、上記で「シェーダに渡す行列を逆行列にする必要が〜」と書きましたが間違いでした。ご提示頂いたコードが正解でした。(何を勘違いしていたのか、すみません)
    また機会があれば、お力添え頂ければ幸いです。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.12%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる