web-dev-qa-db-fra.com

Comment convertir un point 3D en projection perspective 2D?

Je travaille actuellement avec l'utilisation de courbes et de surfaces de Bézier pour dessiner la célèbre théière Utah. En utilisant des correctifs de Bézier de 16 points de contrôle, j'ai pu dessiner la théière et l'afficher en utilisant une fonction `` World to camera '' qui donne la possibilité de faire pivoter la théière résultante, et j'utilise actuellement une projection orthographique.

Le résultat est que j'ai une théière "plate", ce qui est attendu car le but d'une projection orthographique est de préserver les lignes parallèles.

Cependant, je voudrais utiliser une projection en perspective pour donner la profondeur de la théière. Ma question est de savoir comment prendre le sommet 3D xyz revenu de la fonction "monde à la caméra" et le convertir en coordonnées 2D. Je souhaite utiliser le plan de projection à z = 0 et permettre à l'utilisateur de déterminer la distance focale et la taille de l'image à l'aide des touches fléchées du clavier.

Je programme cela en Java et j'ai tout le gestionnaire d'événements d'entrée configuré, et j'ai également écrit une classe de matrice qui gère la multiplication de matrice de base. J'ai lu à travers wikipedia et d'autres ressources pour un moment, mais je ne peux pas vraiment comprendre comment on effectue cette transformation.

50
Zachary Wright

Je vois que cette question est un peu ancienne, mais j'ai quand même décidé de donner une réponse à ceux qui trouvent cette question en cherchant.
La façon standard de représenter les transformations 2D/3D de nos jours est d'utiliser des coordonnées homogènes . [x, y, w] pour 2D et [x, y, z, w] pour 3D. Puisque vous avez trois axes en 3D et en translation, ces informations s'intègrent parfaitement dans une matrice de transformation 4x4. J'utiliserai la notation matricielle des colonnes dans cette explication. Toutes les matrices sont 4x4 sauf indication contraire.
Les étapes à partir de points 3D et vers un point, une ligne ou un polygone tramé ressemblent à ceci:

  1. Transformez vos points 3D avec la matrice de caméra inverse, suivie des transformations dont ils ont besoin. Si vous avez des normales de surface, transformez-les également mais avec w mis à zéro, car vous ne voulez pas traduire les normales. La matrice avec laquelle vous transformez les normales doit être isotrope; la mise à l'échelle et le cisaillement rendent les normales malformées.
  2. Transformez le point avec une matrice d'espace de clip. Cette matrice met à l'échelle x et y avec le champ de vision et le rapport hauteur/largeur, met à l'échelle z par les plans d'écrêtage proches et éloignés et branche le "vieux" z dans w. Après la transformation, vous devez diviser x, y et z par w. C'est ce qu'on appelle la division en perspective.
  3. Vos sommets se trouvent maintenant dans l'espace du clip et vous souhaitez effectuer un découpage afin de ne pas rendre de pixels en dehors des limites de la fenêtre. L'écrêtage Sutherland-Hodgeman est l'algorithme d'écrêtage le plus répandu.
  4. Transformez x et y par rapport à w et la demi-largeur et la mi-hauteur. Vos coordonnées x et y sont désormais en coordonnées de fenêtre. w est ignoré, mais 1/w et z sont généralement enregistrés car 1/w est requis pour effectuer une interpolation correcte en perspective sur la surface du polygone, et z est stocké dans le z-buffer et utilisé pour les tests de profondeur.

Cette étape est la projection réelle, car z n'est plus utilisé comme composant dans la position.

Les algorithmes:

Calcul du champ de vision

Ceci calcule le champ de vision. Que le bronzage prenne des radians ou des degrés n'a pas d'importance, mais angle doit correspondre. Notez que le résultat atteint l'infini lorsque angle se rapproche de 180 degrés. Il s'agit d'une singularité, car il est impossible d'avoir un point focal aussi large. Si vous voulez une stabilité numérique, gardez angle inférieur ou égal à 179 degrés.

fov = 1.0 / tan(angle/2.0)

Notez également que 1.0/tan (45) = 1. Quelqu'un d'autre ici a suggéré de simplement diviser par z. Le résultat ici est clair. Vous obtiendrez un FOV à 90 degrés et un rapport d'aspect de 1: 1. L'utilisation de coordonnées homogènes comme celle-ci présente également plusieurs autres avantages; on peut par exemple effectuer un écrêtage contre les plans proche et lointain sans le traiter comme un cas particulier.

Calcul de la matrice d'écrêtage

Il s'agit de la disposition de la matrice de clip. aspectRatio est la largeur/hauteur. Ainsi, le FOV pour le composant x est mis à l'échelle en fonction du FOV pour y. Loin et proche sont des coefficients qui sont les distances pour les plans d'écrêtage proches et lointains.

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

Projection d'écran

Après l'écrêtage, c'est la transformation finale pour obtenir nos coordonnées d'écran.

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

Exemple d'implémentation triviale en 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;
}

Si vous y réfléchissez encore, la spécification OpenGL est une référence vraiment sympa pour les mathématiques impliquées. Les forums DevMaster à http://www.devmaster.net/ ont aussi beaucoup d'articles Nice liés aux rastériseurs logiciels.

87
Mads Elvheim

Je pense que this répondra probablement à votre question. Voici ce que j'ai écrit là-bas:

Voici une réponse très générale. Dites que la caméra est en (Xc, Yc, Zc) et le point que vous souhaitez projeter est P = (X, Y, Z). La distance entre la caméra et le plan 2D sur lequel vous projetez est F (donc l'équation du plan est Z-Zc = F). Les coordonnées 2D de P projetées sur le plan sont (X ', Y').

Ensuite, très simplement:

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

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

Si votre appareil photo est l'origine, cela se simplifie:

X '= X * (F/Z)

Y '= Y * (F/Z)

13
rofrankel

Vous pouvez projeter un point 3D en 2D en utilisant: Commons Math: The Apache Commons Mathematics Library avec seulement deux classes.

Exemple pour 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));
}

Maintenant, il vous suffit de mettre à jour les planeX et planeY pour changer la perspective-projection, pour obtenir des choses comme ceci:

enter image description hereenter image description here

6
Daniel De León

Pour obtenir les coordonnées corrigées de la perspective, il suffit de diviser par la coordonnée z:

xc = x / z
yc = y / z

Les travaux ci-dessus supposent que la caméra est à (0, 0, 0) et vous projetez sur l'avion à z = 1 - vous devez autrement traduire les coordonnées par rapport à la caméra.

Il y a quelques complications pour les courbes, dans la mesure où la projection des points d'une courbe de Bézier 3D ne vous donnera généralement pas les mêmes points que le dessin d'une courbe de Bézier 2D à travers les points projetés.

5
j_random_hacker

enter image description here

En regardant l'écran du haut, vous obtenez les axes x et z.
En regardant l'écran de côté, vous obtenez les axes y et z.

Calculez les distances focales des vues de dessus et de côté, en utilisant la trigonométrie, qui est la distance entre l'œil et le milieu de l'écran, qui est déterminée par le champ de vision de l'écran. Cela fait la forme de deux triangles rectangles dos à dos.

hw = largeur_écran/2

hh = screen_height/2

fl_top = hw/tan (θ/2)

fl_side = hh/tan (θ/2)


Prenez ensuite la distance focale moyenne.

fl_average = (fl_top + fl_side)/2


Calculez maintenant le nouveau x et le nouveau y avec l'arithmétique de base, car le plus grand triangle rectangle fait à partir du point 3d et du point de l'œil est congru avec le plus petit triangle fait par le point 2d et le point de l'œil.

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

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


Ou vous pouvez simplement définir

x '= x/(z + 1)

et

y '= y/(z + 1)

2
Quinn Fowler

Je ne sais pas à quel niveau vous posez cette question. Il semble que vous ayez trouvé les formules en ligne et que vous essayez simplement de comprendre ce qu'il fait. À la lecture de votre question, je vous propose:

  • Imaginez un rayon du spectateur (au point V) directement vers le centre du plan de projection (appelez-le C).
  • Imaginez un deuxième rayon du spectateur à un point de l'image (P) qui coupe également le plan de projection à un certain point (Q)
  • Le spectateur et les deux points d'intersection sur le plan de vue forment un triangle (VCQ); les côtés sont les deux rayons et la ligne entre les points du plan.
  • Les formules utilisent ce triangle pour trouver les coordonnées de Q, où ira le pixel projeté
1
MarkusQ

Toutes les réponses répondent à la question posée dans le titre. Cependant, je voudrais ajouter une mise en garde implicite dans le texte. Les patchs de Bézier sont utilisés pour représenter la surface, mais vous ne pouvez pas simplement transformer les points du patch et tesseller le patch en polygones, car cela entraînera une géométrie déformée. Vous pouvez cependant tesseler le patch en polygones en utilisant une tolérance d'écran transformée puis transformer les polygones, ou vous pouvez convertir les patchs de Bézier en patchs de Bézier rationnels, puis tesseler ceux en utilisant une tolérance d'espace d'écran. Le premier est plus facile, mais le second est meilleur pour un système de production.

Je soupçonne que vous voulez le moyen le plus simple. Pour cela, vous devez mettre à l'échelle la tolérance d'écran selon la norme du jacobien de la transformation de perspective inverse et l'utiliser pour déterminer la quantité de tessellation dont vous avez besoin dans l'espace objet (il pourrait être plus facile de calculer le jacobien avant, inversez cela, puis prendre la norme). Notez que cette norme dépend de la position et vous souhaiterez peut-être l'évaluer à plusieurs endroits, selon la perspective. Souvenez-vous également que la transformation projective étant rationnelle, vous devez appliquer la règle du quotient pour calculer les dérivées.

1
Reality Pixels

Je sais que c'est un vieux sujet mais votre illustration n'est pas correcte, le code source configure la matrice de clip correctement.

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

quelques ajouts à vos affaires:

Cette matrice de clips ne fonctionne que si vous projetez sur un plan 2D statique si vous souhaitez ajouter un mouvement et une rotation à la caméra:

viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;

cela vous permet de faire pivoter le plan 2D et de le déplacer ..-

0
dazedsheep

Vous voudrez peut-être déboguer votre système avec des sphères pour déterminer si vous avez ou non un bon champ de vision. Si vous l'avez trop large, les sphères se déforment sur les bords de l'écran en formes plus ovales pointées vers le centre du cadre. La solution à ce problème consiste à zoomer sur le cadre, en multipliant les coordonnées x et y pour le point tridimensionnel par un scalaire, puis en rétrécissant votre objet ou votre monde vers le bas d'un facteur similaire. Ensuite, vous obtenez la belle sphère ronde ronde sur tout le cadre.

Je suis presque gêné que cela m'ait pris toute la journée pour comprendre celui-ci et j'étais presque convaincu qu'il y avait un phénomène géométrique mystérieux effrayant qui se passait ici et qui exigeait une approche différente.

Pourtant, l'importance de calibrer le coefficient de zoom du cadre de vue en rendant les sphères ne peut pas être surestimée. Si vous ne savez pas où se trouve la "zone habitable" de votre univers, vous finirez par marcher sur le Soleil et abandonner le projet. Vous voulez pouvoir rendre une sphère n'importe où dans votre cadre de vue et la faire apparaître ronde. Dans mon projet, la sphère unitaire est massive par rapport à la région que je décris.

En outre, l'entrée wikipedia obligatoire: Système de coordonnées sphériques

0
JustKevin