Come convertire un punto 3D in proiezione prospettica 2D?

Attualmente sto lavorando con le curve e le superfici di Bezier per disegnare la famosa teiera dello Utah. Usando patch Bezier di 16 punti di controllo, sono stato in grado di disegnare la teiera e visualizzarla usando una funzione “da mondo a macchina fotografica” che dà la possibilità di ruotare la teiera risultante e attualmente sto usando una proiezione ortografica.

Il risultato è che ho una teiera “piatta”, che è prevista in quanto lo scopo di una proiezione ortografica è di preservare le linee parallele.

Tuttavia, mi piacerebbe utilizzare una proiezione prospettica per dare profondità alla teiera. La mia domanda è: come si può prendere il vertice xyz 3D restituito dalla funzione ‘world to camera’ e convertirlo in una coordinata 2D. Sto volendo usare il piano di proiezione su z = 0 e consentire all’utente di determinare la lunghezza focale e la dimensione dell’immagine usando i tasti freccia sulla tastiera.

Sto programmando questo in java e ho impostato tutto il gestore di eventi di input, e ho anche scritto una class matrix che gestisce la moltiplicazione della matrice di base. Ho letto wikipedia e altre risorse per un po ‘, ma non riesco a capire come si esegue questa trasformazione.

Vedo che questa domanda è un po ‘vecchia, ma ho comunque deciso di dare una risposta a coloro che trovano questa domanda cercando.
Il modo standard per rappresentare le trasformazioni 2D / 3D al giorno d’oggi è l’utilizzo di coordinate omogenee . [x, y, w] per 2D e [x, y, z, w] per 3D. Poiché hai tre assi in 3D e traduzione, questa informazione si adatta perfettamente a una matrice di trasformazione 4×4. Userò la notazione matriciale colonna-maggiore in questa spiegazione. Tutte le matrici sono 4×4 se non diversamente specificato.
Le fasi da punti 3D e ad un punto, una linea o un poligono rasterizzati sono simili a questo:

  1. Trasforma i tuoi punti 3D con la matrice della telecamera inversa, seguendoli con le trasformazioni di cui hanno bisogno. Se hai delle normali di superficie, trasformale pure con w impostato a zero, dato che non vuoi tradurre le normali. La matrice con cui si trasformano le normali deve essere isotropa ; ridimensionamento e tosatura rendono le normali malformate.
  2. Trasforma il punto con una matrice di spazio per i clip. Questa matrice scala xey con il campo di vista e le proporzioni, scala z dai piani di ritaglio vicini e lontani e inserisce la “vecchia” z in w. Dopo la trasformazione, dovresti dividere x, yez di w. Questo è chiamato il divario prospettico .
  3. Ora i tuoi vertici sono nello spazio clip e vuoi eseguire il clipping in modo da non rendere nessun pixel al di fuori dei limiti del viewport. Il ritaglio di Sutherland-Hodgeman è l’algoritmo di ritaglio più diffuso in uso.
  4. Trasforma xey rispetto a w e a metà larghezza e metà altezza. Le coordinate xey sono ora in coordinate viewport. w viene scartato, ma 1 / w e z di solito vengono salvati perché 1 / w è richiesto per eseguire l’interpolazione prospettiva-corretta attraverso la superficie del poligono, e z è memorizzato nel buffer z e utilizzato per il test di profondità.

Questa fase è la proiezione effettiva, poiché z non viene più utilizzato come componente nella posizione.

Gli algoritmi:

Calcolo del campo di vista

Questo calcola il campo di vista. L’abbronzatura prende radianti o gradi è irrilevante, ma l’ angolo deve corrispondere. Si noti che il risultato raggiunge l’infinito quando l’ angolo si avvicina a 180 gradi. Questa è una singolarità, poiché è imansible avere un punto focale così ampio. Se si desidera stabilità numerica, mantenere l’ angolo inferiore o uguale a 179 gradi.

fov = 1.0 / tan(angle/2.0) 

Si noti inoltre che 1.0 / tan (45) = 1. Qualcun altro qui ha suggerito di dividere semplicemente per z. Il risultato qui è chiaro. Otterrai un FOV a 90 gradi e un rapporto aspetto 1: 1. L’uso di coordinate omogenee come questa ha anche molti altri vantaggi; possiamo ad esempio eseguire il clipping contro i piani vicini e lontani senza trattarlo come caso speciale.

Calcolo della matrice di clip

Questo è il layout della matrice di clip. aspectRatio è Larghezza / Altezza. Quindi il FOV per il componente x viene ridimensionato in base al FOV per y. I coefficienti vicini e lontani sono le distanze per i piani di ritaglio vicini e lontani.

 [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 dello 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  #include  #include  #include  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 data; }; inline Vector operator*(const Vector& v, const Matrix& m) { Vector dst; dst.x = vx*m[0] + vy*m[4] + vz*m[8 ] + vw*m[12]; dst.y = vx*m[1] + vy*m[5] + vz*m[9 ] + vw*m[13]; dst.z = vx*m[2] + vy*m[6] + vz*m[10] + vw*m[14]; dst.w = vx*m[3] + vy*m[7] + vz*m[11] + vw*m[15]; return dst; } typedef std::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 /= vw; /* Don't get confused here. I assume the divide leaves vw 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 su questo, la specifica OpenGL è un riferimento davvero interessante per la matematica in questione. I forum DevMaster su http://www.devmaster.net/ contengono anche molti articoli interessanti relativi ai rasterizzatori software.

Penso che questo probabilmente risponda alla tua domanda. Ecco cosa ho scritto lì:

Ecco una risposta molto generale. Dì che la videocamera è a (Xc, Yc, Zc) e il punto che vuoi proiettare è P = (X, Y, Z). La distanza tra la telecamera e il piano 2D su cui si sta proiettando è F (quindi l’equazione del piano è Z-Zc = F). Le coordinate 2D di P proiettate sul piano sono (X ‘, Y’).

Quindi, molto semplicemente:

X ‘= ((X – Xc) * (F / Z)) + Xc

Y ‘= ((Y – Yc) * (F / Z)) + Yc

Se la tua fotocamera è l’origine, allora questo semplifica:

X ‘= X * (F / Z)

Y ‘= Y * (F / Z)

È ansible proiettare il punto 3D in 2D usando: Commons Math: la biblioteca di Mathematics di 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 hai solo bisogno di aggiornare il planeX e il planeY per cambiare la proiezione prospettica, per ottenere cose come questa:

inserisci la descrizione dell'immagine quiinserisci la descrizione dell'immagine qui

Per ottenere le coordinate corrette per la prospettiva, basta dividere per la coordinata z :

 xc = x / z yc = y / z 

Quanto sopra funziona supponendo che la fotocamera sia a (0, 0, 0) e tu stia proiettando sul piano in z = 1 – hai bisogno di tradurre le co-ord relative alla camera altrimenti.

Ci sono alcune complicazioni per le curve, nella misura in cui la proiezione dei punti di una curva 3D di Bezier non ti darà in generale gli stessi punti del disegno di una curva Bezier 2D attraverso i punti proiettati.

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

  • Immagina un raggio dall’osservatore (nel punto V) direttamente verso il centro del piano di proiezione (chiamalo C).
  • Immagina un secondo raggio dall’osservatore a un punto dell’immagine (P) che interseca anche il piano di proiezione in un punto (Q)
  • L’osservatore e i due punti di intersezione sul piano della vista formano un triangolo (VCQ); i lati sono i due raggi e la linea tra i punti nel piano.
  • Le formule stanno usando questo triangolo per trovare le coordinate di Q, che è dove andrà il pixel proiettato

inserisci la descrizione dell'immagine qui

Guardando lo schermo dall’alto, ottieni l’asse x e z.
Guardando lo schermo dal lato, ottieni l’asse yez.

Calcola le lunghezze focali delle viste superiore e laterale, usando la trigonometria, che è la distanza tra l’occhio e il centro dello schermo, che è determinata dal campo visivo dello schermo. Questo rende la forma di due triangoli rettangoli indietro.

hw = screen_width / 2

hh = screen_height / 2

fl_top = hw / tan (θ / 2)

fl_side = hh / tan (θ / 2)

Quindi prendi la lunghezza focale media.

fl_average = (fl_top + fl_side) / 2

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

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

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

O puoi semplicemente impostare

x ‘= x / (z + 1)

e

y ‘= y / (z + 1)

Tutte le risposte affrontano la domanda posta nel titolo . Tuttavia, vorrei aggiungere un avvertimento implicito nel testo . Le patch di Bézier sono utilizzate per rappresentare la superficie, ma non puoi semplicemente trasformare i punti della patch e tessellare la patch in poligoni, perché ciò determinerà una geometria distorta. Puoi, tuttavia, tessellare la patch prima in poligoni usando una tolleranza di schermo trasformata e quindi trasformare i poligoni, oppure puoi convertire le patch di Bézier in patch di Bézier razionali, quindi tessellare quelli usando una tolleranza dello spazio dello schermo. Il primo è più semplice, ma il secondo è migliore per un sistema di produzione.

Sospetto che tu voglia il modo più semplice. Per fare ciò, ridimensionate la tolleranza dello schermo secondo la norma della Jacobian della trasformazione della prospettiva inversa e usate questo per determinare la quantità di tassellatura di cui avete bisogno nello spazio del modello (potrebbe essere più semplice calcolare il Jacobian in avanti, invertirlo, quindi prendere la norma). Si noti che questa norma dipende dalla posizione e potrebbe essere utile valutarla in diverse posizioni, a seconda della prospettiva. Ricorda anche che poiché la trasformazione proiettiva è razionale, devi applicare la regola del quoziente per calcolare le derivate.

So che è un vecchio argomento ma la tua illustrazione non è corretta, il codice sorgente imposta correttamente la matrice del 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 si proietta su un piano 2D statico se si desidera aggiungere movimento e rotazione della telecamera:

 viewMatrix = clipMatrix * cameraTranslationMatrix4x4 * cameraRotationMatrix4x4; 

questo ti permette di ruotare il piano 2D e spostarlo in giro ..-

È ansible eseguire il debug del sistema con sfere per determinare se si dispone o meno di un buon campo visivo. Se lo hai troppo largo, le sfere si deformano ai bordi dello schermo in forms più ovali puntate verso il centro del fotogramma. La soluzione a questo problema è di ingrandire il fotogramma moltiplicando le coordinate xey per il punto tridimensionale di uno scalare e quindi riducendo il tuo object o mondo di un fattore simile. Poi ottieni la bella sfera rotonda su tutto il fotogramma.

Sono quasi imbarazzato dal fatto che mi ci sia voluto tutto il giorno per capirlo e ero quasi convinto che ci fosse un misterioso misterioso fenomeno geometrico che richiedeva un approccio diverso.

Tuttavia, l’importanza di calibrare il coefficiente di zoom-of-view-view rendendo le sfere non può essere sopravvalutata. Se non sai dove si trova la “zona abitabile” del tuo universo, finirai per camminare sul sole e demolire il progetto. Vuoi essere in grado di rendere 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 wikipedia obbligatoria: sistema di coordinate sferiche

Grazie a @Mads Elvenheim per un codice di esempio corretto. Ho corretto gli errori di syntax minori nel codice (solo alcuni problemi const e ovvi operatori mancanti). Inoltre, vicino e lontano hanno significati molto diversi in vs.

Per il tuo piacere, ecco la versione compilabile (MSVC2013). Divertiti. Considera che ho reso costante NEAR_Z e FAR_Z. Probabilmente non lo vorresti.

 #include  #include  #include  #include  #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 data; }; inline Vector operator*( const Vector& v, Matrix& m ) { Vector dst; dst.x = vx*m[0] + vy*m[4] + vz*m[8] + vw*m[12]; dst.y = vx*m[1] + vy*m[5] + vz*m[9] + vw*m[13]; dst.z = vx*m[2] + vy*m[6] + vz*m[10] + vw*m[14]; dst.w = vx*m[3] + vy*m[7] + vz*m[11] + vw*m[15]; return dst; } typedef std::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 /= vw; /* Don't get confused here. I assume the divide leaves vw 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