Domanda

Attualmente sto lavorando utilizzando le curve e le superfici di Bezier per disegnare la famosa teiera dello Utah.Utilizzando patch Bezier di 16 punti di controllo, sono stato in grado di disegnare la teiera e visualizzarla utilizzando una funzione "world to camera" che dà la possibilità di ruotare la teiera risultante e attualmente sto utilizzando una proiezione ortografica.

Il risultato è che ho una teiera 'piatta', come previsto poiché lo scopo di una proiezione ortografica è quello di preservare le linee parallele.

Vorrei però utilizzare una proiezione prospettica per dare profondità alla teiera.La mia domanda è: come si può prendere il vertice xyz 3D restituito dalla funzione "mondo alla telecamera" e convertirlo in una coordinata 2D.Desidero utilizzare il piano di proiezione su z=0 e consentire all'utente di determinare la lunghezza focale e la dimensione dell'immagine utilizzando i tasti freccia sulla tastiera.

Lo sto programmando in Java e ho configurato tutto il gestore degli eventi di input e ho anche scritto una classe di matrice che gestisce la moltiplicazione di matrici di base.Ho letto Wikipedia e altre risorse per un po', ma non riesco a capire come si esegue questa trasformazione.

È stato utile?

Soluzione

Vedo che questa domanda è un po' vecchia, ma ho deciso di dare comunque una risposta per chi trova questa domanda effettuando una ricerca.
Al giorno d'oggi il modo standard per rappresentare le trasformazioni 2D/3D è utilizzare coordinate omogenee. [x,y,w] per 2D e [x,y,z,w] per il 3D.Poiché in 3D sono presenti tre assi oltre alla traduzione, tali informazioni si adattano perfettamente a una matrice di trasformazione 4x4.In questa spiegazione utilizzerò la notazione della matrice della colonna principale.Tutte le matrici sono 4x4 se non diversamente specificato.
Le fasi dai punti 3D a un punto, linea o poligono rasterizzati si presentano così:

  1. Trasforma i tuoi punti 3D con la matrice della fotocamera inversa, seguita da tutte le trasformazioni di cui hanno bisogno.Se hai normali di superficie, trasformale anche tu ma con w impostato su zero, poiché non vuoi tradurre le normali.La matrice con cui trasformi le normali deve esserlo isotropico;il ridimensionamento e il taglio rendono le normali malformate.
  2. Trasforma il punto con una matrice di spazio di clip.Questa matrice scala xey con il campo visivo e le proporzioni, scala z in base ai piani di ritaglio vicino e lontano e inserisce la "vecchia" z in w.Dopo la trasformazione, dovresti dividere x, yez per w.Questo è chiamato il divisione prospettica.
  3. Ora i tuoi vertici sono nello spazio di ritaglio e vuoi eseguire il ritaglio in modo da non eseguire il rendering di pixel all'esterno dei limiti della finestra. Ritaglio Sutherland-Hodgeman è l'algoritmo di ritaglio più diffuso in uso.
  4. Trasforma xey rispetto a w e metà larghezza e metà altezza.Le tue coordinate xey sono ora nelle coordinate del viewport.w viene scartato, ma 1/w e z vengono solitamente salvati perché 1/w è necessario per eseguire l'interpolazione corretta in prospettiva sulla superficie del poligono e z viene memorizzato nel buffer z e utilizzato per il test di profondità.

Questa fase è la proiezione vera e propria, poiché z non viene più utilizzata come componente nella posizione.

Gli algoritmi:

Calcolo del campo visivo

Questo calcola il campo visivo.Ma se l'abbronzatura impiega radianti o gradi è irrilevante angolo deve combaciare.Si noti che il risultato raggiunge l'infinito come angolo si avvicina a 180 gradi.Questa è una singolarità, poiché è impossibile avere un punto focale così ampio.Se vuoi la stabilità numerica, mantieni angolo inferiore o uguale a 179 gradi.

fov = 1.0 / tan(angle/2.0)

Nota anche che 1.0 / tan(45) = 1.Qualcun altro qui ha suggerito di dividere semplicemente per z.Il risultato qui è chiaro.Otterresti un FOV di 90 gradi e un rapporto d'aspetto di 1:1.L'uso di coordinate omogenee come queste presenta anche molti altri vantaggi;possiamo ad esempio eseguire il ritaglio rispetto ai piani vicino e lontano senza trattarlo come un caso speciale.

Calcolo della matrice della clip

Questo è il layout della matrice della clip. proporzioni è Larghezza/Altezza.Quindi il FOV per il componente x viene ridimensionato in base al FOV per y.Lontano e vicino sono coefficienti che rappresentano le distanze per i piani di ritaglio vicino e lontano.

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

Proiezione su schermo

Dopo il ritaglio, questa è la trasformazione finale per ottenere le coordinate dello schermo.

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

Implementazione di esempio banale in 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;
}

Se ci rifletti ancora, le specifiche OpenGL sono davvero un bel riferimento per i calcoli coinvolti.I forum DevMaster su http://www.devmaster.net/ ho anche molti articoli interessanti relativi ai rasterizzatori software.

Altri suggerimenti

Penso Questo probabilmente risponderà alla tua domanda.Ecco cosa ho scritto lì:

Ecco una risposta molto generale.Supponiamo che la telecamera sia su (Xc, Yc, Zc) e che il punto che desideri proiettare sia P = (X, Y, Z).La distanza dalla telecamera al piano 2D su cui stai proiettando è F (quindi l'equazione del piano è Z-Zc=F).Le coordinate 2D di P proiettate sul piano sono (X', Y').

Poi, molto semplicemente:

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

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

Se la tua fotocamera è l'origine, questo si semplifica in:

X' = ​​X * (F/Z)

Y' = Y * (F/Z)

È possibile proiettare punti 3D in 2D utilizzando: Matematica comune:La libreria matematica Apache Commons con solo due classi.

Esempio per 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));
}

Ora devi solo aggiornare il file planeX E planeY per cambiare la proiezione prospettica, per ottenere cose come queste:

enter image description hereenter image description here

Per ottenere le coordinate corrette in prospettiva è sufficiente dividere per z coordinata:

xc = x / z
yc = y / z

Quanto sopra funziona presupponendo che la fotocamera sia accesa (0, 0, 0) e ti stai proiettando sull'aereo a z = 1 -- altrimenti devi tradurre le coordinate relative alla fotocamera.

Ci sono alcune complicazioni per le curve, in quanto proiettare i punti di una curva di Bezier 3D in generale non fornisce gli stessi punti che si ottengono disegnando una curva di Bezier 2D attraverso i punti proiettati.

enter image description here

Guardando lo schermo dall'alto, ottieni gli assi xez.
Guardando lo schermo di lato, ottieni gli assi y e z.

Calcola le lunghezze focali delle viste dall'alto e laterali, utilizzando la trigonometria, che è la distanza tra l'occhio e il centro dello schermo, che è determinata dal campo visivo dello schermo.Questo dà la forma di due triangoli rettangoli uno dietro l'altro.

hw = larghezza_schermo / 2

hh = altezza_schermo / 2

fl_top = hw / tan(θ/2)

lato_fl = hh / tan(θ/2)


Quindi prendi la lunghezza focale media.

fl_media = (fl_top + fl_side) / 2


Ora calcola la nuova x e la nuova y con l'aritmetica di base, poiché il triangolo rettangolo più grande formato dal punto 3d e dal punto dell'occhio è congruente con il triangolo più piccolo formato dal punto 2d e dal punto dell'occhio.

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

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


Oppure puoi semplicemente impostare

x' = x / (z + 1)

E

y' = y / (z + 1)

Non sono sicuro a quale livello stai ponendo questa domanda.Sembra che tu abbia trovato le formule online e stai solo cercando di capire cosa fa.In quella lettura della tua domanda offro:

  • Immagina un raggio dallo spettatore (nel punto V) direttamente verso il centro del piano di proiezione (chiamiamolo C).
  • Immagina un secondo raggio dallo spettatore a un punto dell'immagine (P) che interseca anche il piano di proiezione in un punto (Q)
  • Lo spettatore ed i due punti di intersezione sul piano visivo formano un triangolo (VCQ);i lati sono i due raggi e la linea tra i punti del piano.
  • Le formule utilizzano questo triangolo per trovare le coordinate di Q, che è dove andrà il pixel proiettato

Tutte le risposte rispondono alla domanda posta nel titolo.Vorrei però aggiungere una avvertenza implicita nel testo.Le patch Bézier vengono utilizzate per rappresentare la superficie, ma non è possibile semplicemente trasformare i punti della patch e tassellarla in poligoni, perché ciò risulterà in una geometria distorta.È possibile, tuttavia, tassellare prima la patch in poligoni utilizzando una tolleranza dello schermo trasformata e poi trasformare i poligoni, oppure è possibile convertire le patch Bézier in patch Bézier razionali, quindi tassellarle utilizzando una tolleranza dello spazio sullo schermo.Il primo è più semplice, ma il secondo è migliore per un sistema produttivo.

Sospetto che tu voglia la via più semplice.Per questo, ridimensioneresti la tolleranza dello schermo in base alla norma dello Jacobiano della trasformazione prospettica inversa e la utilizzeresti per determinare la quantità di tassellatura necessaria nello spazio modello (potrebbe essere più semplice calcolare lo Jacobiano diretto, invertirlo, quindi prendi la norma).Tieni presente che questa norma dipende dalla posizione e potresti volerla valutare in diversi punti, a seconda della prospettiva.Ricorda inoltre che poiché la trasformazione proiettiva è razionale, è necessario applicare la regola del quoziente per calcolare le derivate.

So che è un argomento vecchio ma la tua illustrazione non è corretta, il codice sorgente imposta correttamente la matrice della clip.

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

qualche aggiunta alle tue cose:

Questa matrice di clip funziona solo se stai proiettando su un piano 2D statico e desideri aggiungere movimento e rotazione della telecamera:

viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4;

questo ti consente di ruotare il piano 2D e spostarlo...

Potresti voler eseguire il debug del tuo sistema con le sfere per determinare se hai o meno un buon campo visivo.Se lo avete troppo largo, le sfere si deformeranno ai bordi dello schermo in forme più ovali rivolte verso il centro della cornice.La soluzione a questo problema è ingrandire l'inquadratura, moltiplicando le coordinate xey del punto tridimensionale per uno scalare e quindi rimpicciolendo l'oggetto o il mondo di un fattore simile.Quindi ottieni la bella sfera rotonda e uniforme su tutto il fotogramma.

Sono quasi imbarazzato perché mi ci è voluto tutto il giorno per capirlo ed ero quasi convinto che ci fosse qualche misterioso fenomeno geometrico inquietante che richiedeva un approccio diverso.

Tuttavia, l’importanza di calibrare il coefficiente di zoom-frame-of-view eseguendo il rendering delle sfere non può essere sopravvalutata.Se non sai dove si trova la "zona abitabile" del tuo universo, finirai per camminare sul sole e scartare il progetto.Vuoi essere in grado di eseguire il rendering di una sfera in qualsiasi punto del tuo campo visivo e farla apparire rotonda.Nel mio progetto, la sfera unitaria è enorme rispetto alla regione che sto descrivendo.

Inoltre, la voce obbligatoria di Wikipedia:Sistema di coordinate sferiche

Grazie a @Mads Elvenheim per un codice di esempio adeguato.Ho corretto gli errori minori di sintassi nel codice (solo alcuni cost problemi ed evidenti operatori mancanti).Anche, vicino E lontano hanno significati molto diversi in vs.

Per il tuo piacere, ecco la versione compilabile (MSVC2013).Divertiti.Tieni presente che ho reso costanti NEAR_Z e FAR_Z.Probabilmente non lo vuoi così.

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

#define M_PI 3.14159

#define NEAR_Z 0.5
#define FAR_Z 2.5

struct Vector
{
    float x;
    float y;
    float z;
    float w;

    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& operator*=(float fac) noexcept
    {
        x *= fac;
        y *= fac;
        z *= fac;
        return *this;
    }
    Vector  operator*(float fac) const noexcept
    {
        return Vector(*this)*=fac;
    }
    Vector& operator/=(float div) noexcept
    {
        return operator*=(1/div);   // avoid divisions: they are much
                                    // more costly than multiplications
    }

    Vector Unit() const
    {
        const float epsilon = 1e-6;
        float mag = Length();
        if (mag < epsilon) {
            std::out_of_range e( "" );
            throw e;
        }
        return Vector(*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];
    }
    const float& operator[]( size_t index ) const
    {
        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 )
    {
        Identity();
        float f = 1.0f / std::tan( fov * 0.5f );
        data[0] = f*aspectRatio;
        data[5] = f;
        data[10] = (FAR_Z + NEAR_Z) / (FAR_Z- NEAR_Z);
        data[11] = 1.0f; /* this 'plugs' the old z into w */
        data[14] = (2.0f*NEAR_Z*FAR_Z) / (NEAR_Z - FAR_Z);
        data[15] = 0.0f;
    }

    std::vector<float> data;
};


inline Vector operator*( const Vector& v, 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, 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);
    /*  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::const_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;
}
#pragma once
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top