web-dev-qa-db-ja.com

3Dポイントを2D透視投影に変換する方法は?

現在、ベジェ曲線とサーフェスを使用して有名なユタ州のティーポットを描いています。 16のコントロールポイントのベジエパッチを使用して、ティーポットを描画し、結果のティーポットを回転させる「ワールドトゥカメラ」機能を使用して表示することができました。現在、正投影を使用しています。

その結果、正投影の目的は平行線を維持することであるため、「フラットな」ティーポットが用意されています。

ただし、透視投影を使用してティーポットの深さを指定したいと思います。私の質問は、「world to camera」関数から返された3D xyz頂点をどのように取り、これを2D座標に変換するかです。 z = 0の投影面を使用し、ユーザーがキーボードの矢印キーを使用して焦点距離と画像サイズを決定できるようにします。

私はこれをJavaでプログラミングし、すべての入力イベントハンドラーをセットアップし、基本的なマトリックス乗算を処理するマトリックスクラスも作成しました。ウィキペディアやその他のリソースを読んでいます。しばらくですが、この変換をどのように実行するかについて、私はまったく理解できません。

50
Zachary Wright

この質問は少し古いと思いますが、とにかく検索してこの質問を見つけた人には答えを出すことにしました。
現在の2D/3D変換を表す標準的な方法は、同次座標を使用することです。 2Dでは[x、y、w]、3Dでは[x、y、z、w]。 3Dと平行移動の3つの軸があるため、その情報は4x4変換行列に完全に適合します。この説明では、列優先行列表記を使用します。特に記載がない限り、すべての行列は4x4です。
3Dポイントからラスタライズされたポイント、ライン、またはポリゴンまでのステージは次のようになります。

  1. 逆カメラマトリックスを使用して3Dポイントを変換し、必要な変換を実行します。表面の法線がある場合は、それらも変換しますが、法線を変換したくないので、wをゼロに設定します。法線を変換するマトリックスはisotropicでなければなりません。スケーリングとせん断により、法線の形式が崩れます。
  2. クリップ空間行列でポイントを変換します。このマトリックスは、xとyを視野とアスペクト比でスケーリングし、zをニアおよびファークリッピングプレーンでスケーリングし、「古い」zをwにプラグインします。変換後、x、y、zをwで除算する必要があります。これはパースペクティブ除算と呼ばれます。
  3. これで頂点がクリップスペースに配置され、ビューポート境界の外側のピクセルをレンダリングしないようにクリッピングを実行したいです。 Sutherland-Hodgemanクリッピングは、最も普及しているクリッピングアルゴリズムです。
  4. Wと半値幅と半高さに関してxとyを変換します。これで、x座標とy座標はビューポート座標になりました。 wは破棄されますが、ポリゴンサーフェス全体の透視補正補間を行うには1/wが必要であり、zバッファに格納されて深度テストに使用されるため、通常は1/wとzが保存されます。

この段階は、zがその位置のコンポーネントとして使用されなくなったため、実際の投影です。

アルゴリズム:

視野の計算

これにより、視野が計算されます。 tanがラジアンと度のどちらをとるかは関係ありませんが、angleは一致する必要があります。 angleが180度に近づくと、結果が無限に達することに注意してください。これほど広い焦点を結ぶことは不可能なので、これは特異点です。数値の安定性が必要な場合は、angleを179度以下に保ちます。

fov = 1.0 / tan(angle/2.0)

また、1.0/tan(45)= 1であることに注意してください。ここで他の誰かがzで除算することを提案しました。ここでの結果は明らかです。 90度の視野とアスペクト比1:1が得られます。このような同次座標を使用すると、他にもいくつかの利点があります。たとえば、特殊なケースとして扱うことなく、ニアプレーンとファープレーンに対してクリッピングを実行できます。

クリップ行列の計算

これは、クリップマトリックスのレイアウトです。 aspectRatioは幅/高さです。したがって、xコンポーネントのFOVは、yのFOVに基づいてスケーリングされます。 farおよびnearは、nearおよびfarクリッピングプレーンの距離である係数です。

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][        1       ]
[        0        ][        0        ][(2*near*far)/(near-far)][        0       ]

スクリーン投影

クリッピング後、これは画面座標を取得するための最終的な変換です。

new_x = (x * Width ) / (2.0 * w) + halfWidth;
new_y = (y * Height) / (2.0 * w) + halfHeight;

C++での簡単な実装例

#include <vector>
#include <cmath>
#include <stdexcept>
#include <algorithm>

struct Vector
{
    Vector() : x(0),y(0),z(0),w(1){}
    Vector(float a, float b, float c) : x(a),y(b),z(c),w(1){}

    /* Assume proper operator overloads here, with vectors and scalars */
    float Length() const
    {
        return std::sqrt(x*x + y*y + z*z);
    }

    Vector Unit() const
    {
        const float epsilon = 1e-6;
        float mag = Length();
        if(mag < epsilon){
            std::out_of_range e("");
            throw e;
        }
        return *this / mag;
    }
};

inline float Dot(const Vector& v1, const Vector& v2)
{
    return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
}

class Matrix
{
    public:
    Matrix() : data(16)
    {
        Identity();
    }
    void Identity()
    {
        std::fill(data.begin(), data.end(), float(0));
        data[0] = data[5] = data[10] = data[15] = 1.0f;
    }
    float& operator[](size_t index)
    {
        if(index >= 16){
            std::out_of_range e("");
            throw e;
        }
        return data[index];
    }
    Matrix operator*(const Matrix& m) const
    {
        Matrix dst;
        int col;
        for(int y=0; y<4; ++y){
            col = y*4;
            for(int x=0; x<4; ++x){
                for(int i=0; i<4; ++i){
                    dst[x+col] += m[i+col]*data[x+i*4];
                }
            }
        }
        return dst;
    }
    Matrix& operator*=(const Matrix& m)
    {
        *this = (*this) * m;
        return *this;
    }

    /* The interesting stuff */
    void SetupClipMatrix(float fov, float aspectRatio, float near, float far)
    {
        Identity();
        float f = 1.0f / std::tan(fov * 0.5f);
        data[0] = f*aspectRatio;
        data[5] = f;
        data[10] = (far+near) / (far-near);
        data[11] = 1.0f; /* this 'plugs' the old z into w */
        data[14] = (2.0f*near*far) / (near-far);
        data[15] = 0.0f;
    }

    std::vector<float> data;
};

inline Vector operator*(const Vector& v, const Matrix& m)
{
    Vector dst;
    dst.x = v.x*m[0] + v.y*m[4] + v.z*m[8 ] + v.w*m[12];
    dst.y = v.x*m[1] + v.y*m[5] + v.z*m[9 ] + v.w*m[13];
    dst.z = v.x*m[2] + v.y*m[6] + v.z*m[10] + v.w*m[14];
    dst.w = v.x*m[3] + v.y*m[7] + v.z*m[11] + v.w*m[15];
    return dst;
}

typedef std::vector<Vector> VecArr;
VecArr ProjectAndClip(int width, int height, float near, float far, const VecArr& vertex)
{
    float halfWidth = (float)width * 0.5f;
    float halfHeight = (float)height * 0.5f;
    float aspect = (float)width / (float)height;
    Vector v;
    Matrix clipMatrix;
    VecArr dst;
    clipMatrix.SetupClipMatrix(60.0f * (M_PI / 180.0f), aspect, near, far);
    /*  Here, after the perspective divide, you perform Sutherland-Hodgeman clipping 
        by checking if the x, y and z components are inside the range of [-w, w].
        One checks each vector component seperately against each plane. Per-vertex
        data like colours, normals and texture coordinates need to be linearly
        interpolated for clipped edges to reflect the change. If the Edge (v0,v1)
        is tested against the positive x plane, and v1 is outside, the interpolant
        becomes: (v1.x - w) / (v1.x - v0.x)
        I skip this stage all together to be brief.
    */
    for(VecArr::iterator i=vertex.begin(); i!=vertex.end(); ++i){
        v = (*i) * clipMatrix;
        v /= v.w; /* Don't get confused here. I assume the divide leaves v.w alone.*/
        dst.Push_back(v);
    }

    /* TODO: Clipping here */

    for(VecArr::iterator i=dst.begin(); i!=dst.end(); ++i){
        i->x = (i->x * (float)width) / (2.0f * i->w) + halfWidth;
        i->y = (i->y * (float)height) / (2.0f * i->w) + halfHeight;
    }
    return dst;
}

まだこれについて熟考しているのであれば、OpenGLの仕様は関係する数学のための本当に素晴らしいリファレンスです。 http://www.devmaster.net/ のDevMasterフォーラムには、ソフトウェアラスタライザーに関連する多くの素敵な記事があります。

87
Mads Elvheim

this がおそらくあなたの質問に答えると思います。ここに私が書いたものがあります:

これは非常に一般的な答えです。カメラの位置を(Xc、Yc、Zc)とし、投影するポイントはP =(X、Y、Z)です。カメラから投影する2D平面までの距離はFです(したがって、平面の方程式はZ-Zc = Fです)。平面に投影されたPの2D座標は(X '、Y')です。

次に、非常に簡単に:

X '=((X-Xc)*(F/Z))+ Xc

Y '=((Y-Yc)*(F/Z))+ Yc

カメラがOriginの場合、これは次のように単純化されます。

X '= X *(F/Z)

Y '= Y *(F/Z)

13
rofrankel

Commons Math:Apache Commons Mathematics Library を2つのクラスで使用して、2Dで3Dポイントを投影できます。

Java Swing。

import org.Apache.commons.math3.geometry.euclidean.threed.Plane;
import org.Apache.commons.math3.geometry.euclidean.threed.Vector3D;


Plane planeX = new Plane(new Vector3D(1, 0, 0));
Plane planeY = new Plane(new Vector3D(0, 1, 0)); // Must be orthogonal plane of planeX

void drawPoint(Graphics2D g2, Vector3D v) {
    g2.drawLine(0, 0,
            (int) (world.unit * planeX.getOffset(v)),
            (int) (world.unit * planeY.getOffset(v)));
}

protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    drawPoint(g2, new Vector3D(2, 1, 0));
    drawPoint(g2, new Vector3D(0, 2, 0));
    drawPoint(g2, new Vector3D(0, 0, 2));
    drawPoint(g2, new Vector3D(1, 1, 1));
}

次は、planeXplaneYを更新して、透視投影を変更し、次のようなものを取得するだけです。

enter image description hereenter image description here

6
Daniel De León

遠近補正された座標を取得するには、z座標で除算するだけです。

xc = x / z
yc = y / z

上記は、カメラが(0, 0, 0)そして、あなたはz = 1-それ以外の場合は、カメラを基準にして座標を変換する必要があります。

一般に、3Dベジェ曲線のポイントを投影しても、投影ポイントを介して2Dベジェ曲線を描画するのと同じポイントが得られない限り、曲線にはいくつかの問題があります。

5
j_random_hacker

enter image description here

画面を上から見ると、x軸とz軸がわかります。
画面を横から見ると、y軸とz軸が表示されます。

三角法を使用して、トップビューとサイドビューの焦点距離を計算します。三角法は、目と画面の中央との間の距離で、画面の視野によって決まります。これにより、2つの直角三角形の形が背中合わせになります。

hw = screen_width/2

hh = screen_height/2

fl_top = hw/tan(θ/ 2)

fl_side = hh/tan(θ/ 2)


次に、平均焦点距離を取ります。

fl_average =(fl_top + fl_side)/ 2


3x点と視点から作られた大きな直角三角形は、2d点と視点から作られた小さい三角形と一致するため、新しいxと新しいyを基本的な計算で計算します。

x '=(x * fl_top)/(z + fl_top)

y '=(y * fl_top)/(z + fl_top)


または単に設定することができます

x '= x /(z + 1)

そして

y '= y /(z + 1)

2
Quinn Fowler

この質問をどのレベルで聞いているのかわかりません。公式をオンラインで見つけたように聞こえますが、それが何をするのかを理解しようとしているだけです。私が提供するあなたの質問のその読書で:

  • ビューアーから(点Vで)投影面の中心に直接向かう光線(Cと呼ぶ)を想像してください。
  • 視聴者から画像(P)への2番目の光線が、ある点(Q)で投影面と交差することを想像してください
  • ビューアー上のビューアーと2つの交点が三角形(VCQ)を形成します。側面は2つの光線と、平面内のポイント間の線です。
  • 式はこの三角形を使用してQの座標を見つけます。Qは投影されたピクセルの移動先です
1
MarkusQ

すべての回答は、提起された質問に対応していますタイトル内。ただし、暗黙的な本文中という警告を追加したいと思います。ベジエパッチは、サーフェスを表すために使用されますが、パッチのポイントを変形し、パッチをポリゴンにテッセレーションすることはできません。これにより、ジオメトリが歪むためです。ただし、変換されたスクリーントレランスを使用して最初にパッチをポリゴンにテッセレーションしてからポリゴンを変換するか、ベジェパッチを合理的なベジエパッチに変換してから、スクリーンスペーストレランスを使用してテッセレーションすることができます。前者の方が簡単ですが、後者の方が運用システムに適しています。

もっと簡単な方法が欲しいと思う。このために、逆透視変換のヤコビアンのノルムによって画面の許容値をスケーリングし、それを使用してモデル空間で必要なテッセレーションの量を決定します(フォワードヤコビアンを計算し、それを反転してから、規範を取る)。この基準は位置に依存することに注意してください。視点に応じて、いくつかの場所でこれを評価することができます。また、射影変換は合理的であるため、微分を計算するために商ルールを適用する必要があることも覚えておいてください。

1
Reality Pixels

古いトピックであることは知っていますが、イラストは正しくありません。ソースコードによってクリップマトリックスが正しく設定されています。

[fov * aspectRatio][        0        ][        0              ][        0       ]
[        0        ][       fov       ][        0              ][        0       ]
[        0        ][        0        ][(far+near)/(far-near)  ][(2*near*far)/(near-far)]
[        0        ][        0        ][        1              ][        0       ]

あなたのものにいくつかの追加:

このクリップマトリックスは、カメラの移動と回転を追加する場合に静的な2D平面に投影する場合にのみ機能します。

viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;

これにより、2D平面を回転させて移動できます。

0
dazedsheep

球面を使用してシステムをデバッグして、適切な視野があるかどうかを判断することをお勧めします。幅が広すぎると、画面の端にある球体がフレームの中心に向かってより楕円形に変形します。この問題の解決策は、3次元ポイントのx座標とy座標にスカラーを乗算し、同様の係数でオブジェクトまたはワールドを縮小することにより、フレームを拡大することです。次に、フレーム全体に丸い球体を作成します。

これを理解するのに1日中かかったのはほとんど恥ずかしいことであり、別のアプローチを必要とする不気味な不思議な幾何学的現象がここで起こっていると確信しました。

ただし、球体をレンダリングすることによりズームフレーム係数を調整することの重要性は誇張することはできません。宇宙の「居住可能ゾーン」がどこにあるかわからない場合、太陽の上を歩いてプロジェクトを廃棄することになります。視界内のどこにでも球体をレンダリングして、丸く見えるようにする必要があります。私のプロジェクトでは、私が説明している地域に比べて、単位球体は巨大です。

また、ウィキペディアの必須エントリ: Spherical Coordinate System

0
JustKevin